Compare commits

..

75 Commits

Author SHA1 Message Date
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
standardci 0d40ef6796 chore(release): publish new version
- @standardnotes/analytics@2.25.13
 - @standardnotes/auth-server@1.131.4
 - @standardnotes/common@1.50.1
 - @standardnotes/home-server@1.13.44
 - @standardnotes/revisions-server@1.26.7
 - @standardnotes/syncing-server@1.78.7
 - @standardnotes/websockets-server@1.10.17
2023-08-11 07:35:15 +00:00
Mo 1be33ba4c3 refactor: remove unused functions (#694)
* refactor: remove unused functions

* refactor: remove unused functions
2023-08-11 08:58:39 +02:00
Mo aaeb311928 chore: reduce ci revisions timeout 2023-08-10 13:09:49 -05:00
standardci a7a38c07ac chore(release): publish new version
- @standardnotes/home-server@1.13.43
 - @standardnotes/syncing-server@1.78.6
2023-08-10 11:37:24 +00:00
Karol Sójko 56f49752b4 fix(syncing-server): setting user uuid in notifications 2023-08-10 13:04:51 +02:00
Mo 892d8b6fe2 chore: update email template 2023-08-10 05:49:30 -05:00
standardci cec2005436 chore(release): publish new version
- @standardnotes/analytics@2.25.12
 - @standardnotes/api-gateway@1.70.4
 - @standardnotes/auth-server@1.131.3
 - @standardnotes/domain-core@1.25.2
 - @standardnotes/event-store@1.11.19
 - @standardnotes/files-server@1.20.3
 - @standardnotes/home-server@1.13.42
 - @standardnotes/revisions-server@1.26.6
 - @standardnotes/scheduler-server@1.20.21
 - @standardnotes/settings@1.21.24
 - @standardnotes/syncing-server@1.78.5
 - @standardnotes/websockets-server@1.10.16
2023-08-09 16:31:35 +00:00
Karol Sójko 0eb86c0096 Revert "tmp: disable fetching shared vault items"
This reverts commit 18eddea6f8.
2023-08-09 18:01:16 +02:00
Karol Sójko b8e39d76c1 Revert "tmp: skip ci"
This reverts commit f8c9e67063.
2023-08-09 18:01:09 +02:00
Karol Sójko 1c3ff526b7 Revert "Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)""
This reverts commit d261c81cd0.
2023-08-09 18:00:49 +02:00
standardci 373767248c chore(release): publish new version
- @standardnotes/home-server@1.13.41
 - @standardnotes/syncing-server@1.78.4
2023-08-09 15:47:05 +00:00
Karol Sójko d7965b2748 fix(syncing-server): casting handlers 2023-08-09 17:40:48 +02:00
Karol Sójko cbcd2ec87a Revert "Revert "fix(syncing-server): update storage quota used in a shared vault (#691)""
This reverts commit 66f9352a06.
2023-08-09 17:36:59 +02:00
standardci c74d37fc48 chore(release): publish new version
- @standardnotes/home-server@1.13.40
 - @standardnotes/syncing-server@1.78.3
2023-08-09 15:29:30 +00:00
Karol Sójko 66f9352a06 Revert "fix(syncing-server): update storage quota used in a shared vault (#691)"
This reverts commit 3415cae093.
2023-08-09 17:21:59 +02:00
standardci e5eef3aba0 chore(release): publish new version
- @standardnotes/analytics@2.25.11
 - @standardnotes/api-gateway@1.70.3
 - @standardnotes/auth-server@1.131.2
 - @standardnotes/domain-core@1.25.1
 - @standardnotes/event-store@1.11.18
 - @standardnotes/files-server@1.20.2
 - @standardnotes/home-server@1.13.39
 - @standardnotes/revisions-server@1.26.5
 - @standardnotes/scheduler-server@1.20.20
 - @standardnotes/settings@1.21.23
 - @standardnotes/syncing-server@1.78.2
 - @standardnotes/websockets-server@1.10.15
2023-08-09 14:51:38 +00:00
Karol Sójko d261c81cd0 Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)"
This reverts commit 46867c1a4d.
2023-08-09 16:43:33 +02:00
standardci 634e3bbb67 chore(release): publish new version
- @standardnotes/home-server@1.13.38
 - @standardnotes/syncing-server@1.78.1
2023-08-09 14:41:32 +00:00
Karol Sójko f8c9e67063 tmp: skip ci 2023-08-09 16:33:59 +02:00
Karol Sójko 18eddea6f8 tmp: disable fetching shared vault items 2023-08-09 16:17:53 +02:00
301 changed files with 6181 additions and 1740 deletions
+7 -1
View File
@@ -10,7 +10,7 @@ REDIS_HOST=cache
AUTH_SERVER_ACCESS_TOKEN_AGE=4 AUTH_SERVER_ACCESS_TOKEN_AGE=4
AUTH_SERVER_REFRESH_TOKEN_AGE=10 AUTH_SERVER_REFRESH_TOKEN_AGE=10
AUTH_SERVER_EPHEMERAL_SESSION_AGE=300 AUTH_SERVER_EPHEMERAL_SESSION_AGE=300
SYNCING_SERVER_REVISIONS_FREQUENCY=5 SYNCING_SERVER_REVISIONS_FREQUENCY=2
AUTH_SERVER_LOG_LEVEL=debug AUTH_SERVER_LOG_LEVEL=debug
SYNCING_SERVER_LOG_LEVEL=debug SYNCING_SERVER_LOG_LEVEL=debug
FILES_SERVER_LOG_LEVEL=debug FILES_SERVER_LOG_LEVEL=debug
@@ -22,6 +22,12 @@ MYSQL_USER=std_notes_user
MYSQL_PASSWORD=changeme123 MYSQL_PASSWORD=changeme123
MYSQL_ROOT_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_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060 AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
+35 -18
View File
@@ -19,7 +19,12 @@ on:
jobs: jobs:
e2e: 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 runs-on: ubuntu-latest
services: services:
@@ -45,12 +50,14 @@ jobs:
env: env:
DB_TYPE: mysql DB_TYPE: mysql
CACHE_TYPE: redis CACHE_TYPE: redis
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
- name: Wait for server to start - name: Wait for server to start
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
- name: Run E2E Test Suite - name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=enabled
- name: Show logs on failure - name: Show logs on failure
if: ${{ failure() }} if: ${{ failure() }}
@@ -67,13 +74,8 @@ jobs:
matrix: matrix:
db_type: [mysql, sqlite] db_type: [mysql, sqlite]
cache_type: [redis, memory] cache_type: [redis, memory]
include: secondary_db_enabled: [true, false]
- cache_type: redis transition_mode_enabled: [true, false]
db_type: mysql
redis_port: 6380
- cache_type: redis
db_type: sqlite
redis_port: 6381
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -85,16 +87,24 @@ jobs:
cache: cache:
image: redis image: redis
ports: ports:
- ${{ matrix.redis_port }}:6379 - 6379:6379
db: db:
image: mysql image: mysql
ports: ports:
- 3307:3306 - 3306:3306
env: env:
MYSQL_ROOT_PASSWORD: root MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: standardnotes_${{ matrix.cache_type }} MYSQL_DATABASE: standardnotes
MYSQL_USER: standardnotes MYSQL_USER: standardnotes
MYSQL_PASSWORD: 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: steps:
- uses: actions/checkout@v3 - 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 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 "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> 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_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3307" >> packages/home-server/.env echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes_${{ matrix.cache_type }}" >> packages/home-server/.env echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=sqlite_${{ matrix.cache_type }}.db" >> 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_USERNAME=standardnotes" >> packages/home-server/.env
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
echo "DB_TYPE=${{ matrix.db_type }}" >> 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 "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 "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
echo "E2E_TESTING=true" >> packages/home-server/.env echo "E2E_TESTING=true" >> packages/home-server/.env
@@ -145,7 +162,7 @@ jobs:
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
- name: Run E2E Test Suite - name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=enabled
- name: Show logs on failure - name: Show logs on failure
if: ${{ failure() }} if: ${{ failure() }}
Generated
+229 -1
View File
@@ -5191,6 +5191,7 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\ ["inversify-express-utils", "npm:6.4.3"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\ ["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["jsonwebtoken", "npm:9.0.0"],\ ["jsonwebtoken", "npm:9.0.0"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mysql2", "npm:3.3.3"],\ ["mysql2", "npm:3.3.3"],\
["newrelic", "npm:10.1.2"],\ ["newrelic", "npm:10.1.2"],\
["nodemon", "npm:2.0.22"],\ ["nodemon", "npm:2.0.22"],\
@@ -5201,7 +5202,7 @@ const RAW_RUNTIME_STATE =
["semver", "npm:7.5.1"],\ ["semver", "npm:7.5.1"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\ ["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\ ["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.16"],\ ["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\ ["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\ ["ua-parser-js", "npm:1.0.35"],\
["uuid", "npm:9.0.0"],\ ["uuid", "npm:9.0.0"],\
@@ -5869,6 +5870,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "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", [\ ["@types/yargs", [\
["npm:17.0.24", {\ ["npm:17.0.24", {\
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\ "packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\
@@ -7074,6 +7095,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["bson", [\
["npm:5.4.0", {\
"packageLocation": "./.yarn/cache/bson-npm-5.4.0-2f854c8216-2c913a45c0.zip/node_modules/bson/",\
"packageDependencies": [\
["bson", "npm:5.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["buffer", [\ ["buffer", [\
["npm:5.7.1", {\ ["npm:5.7.1", {\
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\ "packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\
@@ -11932,6 +11962,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "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", [\ ["meow", [\
["npm:8.1.2", {\ ["npm:8.1.2", {\
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\ "packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\
@@ -12290,6 +12329,59 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["mongodb", [\
["npm:5.7.0", {\
"packageLocation": "./.yarn/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "npm:5.7.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0", {\
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-eb0cd47e23/0/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["@aws-sdk/credential-providers", null],\
["@mongodb-js/zstd", null],\
["@types/aws-sdk__credential-providers", null],\
["@types/kerberos", null],\
["@types/mongodb-client-encryption", null],\
["@types/mongodb-js__zstd", null],\
["@types/snappy", null],\
["bson", "npm:5.4.0"],\
["kerberos", null],\
["mongodb-client-encryption", null],\
["mongodb-connection-string-url", "npm:2.6.0"],\
["saslprep", "npm:1.0.3"],\
["snappy", null],\
["socks", "npm:2.7.1"]\
],\
"packagePeers": [\
"@aws-sdk/credential-providers",\
"@mongodb-js/zstd",\
"@types/aws-sdk__credential-providers",\
"@types/kerberos",\
"@types/mongodb-client-encryption",\
"@types/mongodb-js__zstd",\
"@types/snappy",\
"kerberos",\
"mongodb-client-encryption",\
"snappy"\
],\
"linkType": "HARD"\
}]\
]],\
["mongodb-connection-string-url", [\
["npm:2.6.0", {\
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-8a9186dd1b.zip/node_modules/mongodb-connection-string-url/",\
"packageDependencies": [\
["mongodb-connection-string-url", "npm:2.6.0"],\
["@types/whatwg-url", "npm:8.2.2"],\
["whatwg-url", "npm:11.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ms", [\ ["ms", [\
["npm:2.0.0", {\ ["npm:2.0.0", {\
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\ "packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\
@@ -14249,6 +14341,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["saslprep", [\
["npm:1.0.3", {\
"packageLocation": "./.yarn/cache/saslprep-npm-1.0.3-8db649c346-23ebcda091.zip/node_modules/saslprep/",\
"packageDependencies": [\
["saslprep", "npm:1.0.3"],\
["sparse-bitfield", "npm:3.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["schema-utils", [\ ["schema-utils", [\
["npm:3.1.2", {\ ["npm:3.1.2", {\
"packageLocation": "./.yarn/cache/schema-utils-npm-3.1.2-d97c6dc247-11d35f997e.zip/node_modules/schema-utils/",\ "packageLocation": "./.yarn/cache/schema-utils-npm-3.1.2-d97c6dc247-11d35f997e.zip/node_modules/schema-utils/",\
@@ -14604,6 +14706,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "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", [\ ["spawn-please", [\
["npm:2.0.1", {\ ["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\ "packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\
@@ -15246,6 +15358,14 @@ const RAW_RUNTIME_STATE =
["tr46", "npm:0.0.3"]\ ["tr46", "npm:0.0.3"]\
],\ ],\
"linkType": "HARD"\ "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", [\ ["treeverse", [\
@@ -15757,6 +15877,98 @@ const RAW_RUNTIME_STATE =
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\ }],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-13b6364fde/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", null],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\
["@types/ioredis", null],\
["@types/mongodb", null],\
["@types/mssql", null],\
["@types/mysql2", null],\
["@types/oracledb", null],\
["@types/pg", null],\
["@types/pg-native", null],\
["@types/pg-query-stream", null],\
["@types/redis", null],\
["@types/sap__hana-client", null],\
["@types/sql.js", null],\
["@types/sqlite3", null],\
["@types/ts-node", null],\
["@types/typeorm-aurora-data-api-driver", null],\
["app-root-path", "npm:3.1.0"],\
["better-sqlite3", null],\
["buffer", "npm:6.0.3"],\
["chalk", "npm:4.1.2"],\
["cli-highlight", "npm:2.1.11"],\
["date-fns", "npm:2.30.0"],\
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"],\
["dotenv", "npm:16.1.3"],\
["glob", "npm:8.1.0"],\
["hdb-pool", null],\
["ioredis", null],\
["mkdirp", "npm:2.1.6"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mssql", null],\
["mysql2", "npm:3.3.3"],\
["oracledb", null],\
["pg", null],\
["pg-native", null],\
["pg-query-stream", null],\
["redis", null],\
["reflect-metadata", "npm:0.1.13"],\
["sha.js", "npm:2.4.11"],\
["sql.js", null],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-node", null],\
["tslib", "npm:2.5.2"],\
["typeorm-aurora-data-api-driver", null],\
["uuid", "npm:9.0.0"],\
["yargs", "npm:17.7.2"]\
],\
"packagePeers": [\
"@google-cloud/spanner",\
"@sap/hana-client",\
"@types/better-sqlite3",\
"@types/google-cloud__spanner",\
"@types/hdb-pool",\
"@types/ioredis",\
"@types/mongodb",\
"@types/mssql",\
"@types/mysql2",\
"@types/oracledb",\
"@types/pg-native",\
"@types/pg-query-stream",\
"@types/pg",\
"@types/redis",\
"@types/sap__hana-client",\
"@types/sql.js",\
"@types/sqlite3",\
"@types/ts-node",\
"@types/typeorm-aurora-data-api-driver",\
"better-sqlite3",\
"hdb-pool",\
"ioredis",\
"mongodb",\
"mssql",\
"mysql2",\
"oracledb",\
"pg-native",\
"pg-query-stream",\
"pg",\
"redis",\
"sql.js",\
"sqlite3",\
"ts-node",\
"typeorm-aurora-data-api-driver"\
],\
"linkType": "HARD"\
}],\
["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.16", {\ ["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-fc9b7b780b/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\ "packageLocation": "./.yarn/__virtual__/typeorm-virtual-fc9b7b780b/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\ "packageDependencies": [\
@@ -16191,6 +16403,13 @@ const RAW_RUNTIME_STATE =
["webidl-conversions", "npm:3.0.1"]\ ["webidl-conversions", "npm:3.0.1"]\
],\ ],\
"linkType": "HARD"\ "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", [\ ["webpack", [\
@@ -16249,6 +16468,15 @@ const RAW_RUNTIME_STATE =
}]\ }]\
]],\ ]],\
["whatwg-url", [\ ["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", {\ ["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\ "packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\
"packageDependencies": [\ "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.
Binary file not shown.
+17
View File
@@ -23,6 +23,8 @@ services:
environment: environment:
DB_TYPE: "${DB_TYPE}" DB_TYPE: "${DB_TYPE}"
CACHE_TYPE: "${CACHE_TYPE}" CACHE_TYPE: "${CACHE_TYPE}"
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
TRANSITION_MODE_ENABLED: "${TRANSITION_MODE_ENABLED}"
container_name: server-ci container_name: server-ci
ports: ports:
- 3123:3000 - 3123:3000
@@ -61,6 +63,21 @@ services:
networks: networks:
- standardnotes_self_hosted - 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: cache:
image: redis:6.0-alpine image: redis:6.0-alpine
container_name: cache-ci container_name: cache-ci
+8
View File
@@ -2,6 +2,8 @@
# Setup environment variables # Setup environment variables
export MODE="self-hosted"
######### #########
# PORTS # # PORTS #
######### #########
@@ -63,6 +65,12 @@ fi
if [ -z "$CACHE_TYPE" ]; then if [ -z "$CACHE_TYPE" ]; then
export CACHE_TYPE="redis" export CACHE_TYPE="redis"
fi 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" 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 done:"
echo "$LINKING_RESULT" echo "$LINKING_RESULT"
echo "linking topic $SYNCING_SERVER_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN" echo "linking topic $FILES_TOPIC_ARN to queue $SYNCING_SERVER_QUEUE_ARN"
LINKING_RESULT=$(link_queue_and_topic $SYNCING_SERVER_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN) LINKING_RESULT=$(link_queue_and_topic $FILES_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking done:" echo "linking done:"
echo "$LINKING_RESULT" 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" 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) LINKING_RESULT=$(link_queue_and_topic $AUTH_TOPIC_ARN $SYNCING_SERVER_QUEUE_ARN)
echo "linking done:" echo "linking done:"
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.16...@standardnotes/analytics@2.25.17) (2023-08-24)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.11...@standardnotes/analytics@2.25.12) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.10...@standardnotes/analytics@2.25.11) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.9...@standardnotes/analytics@2.25.10) (2023-08-09) ## [2.25.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.9...@standardnotes/analytics@2.25.10) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics **Note:** Version bump only for package @standardnotes/analytics
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/analytics", "name": "@standardnotes/analytics",
"version": "2.25.10", "version": "2.25.17",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "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 LOG_LEVEL=debug
NODE_ENV=development NODE_ENV=development
VERSION=development VERSION=development
+34
View File
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.72.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.72.0...@standardnotes/api-gateway@1.72.1) (2023-08-28)
### Bug Fixes
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/api-gateway/issues/714)) ([aef9254](https://github.com/standardnotes/api-gateway/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
# [1.72.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.1...@standardnotes/api-gateway@1.72.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/api-gateway/issues/707)) ([05bb12c](https://github.com/standardnotes/api-gateway/commit/05bb12c97899824f06e6d01d105dec75fc328440))
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.2...@standardnotes/api-gateway@1.70.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.1...@standardnotes/api-gateway@1.70.2) (2023-08-09) ## [1.70.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.1...@standardnotes/api-gateway@1.70.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway **Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/api-gateway", "name": "@standardnotes/api-gateway",
"version": "1.70.2", "version": "1.72.1",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "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 authHeaderValue = request.headers.authorization as string
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
const cacheKey = `${authHeaderValue}${
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
}`
try { try {
let crossServiceTokenFetchedFromCache = true let crossServiceTokenFetchedFromCache = true
let crossServiceToken = null let crossServiceToken = null
if (this.crossServiceTokenCacheTTL) { if (this.crossServiceTokenCacheTTL) {
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue) crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
} }
if (crossServiceToken === null) { if (this.crossServiceTokenIsEmptyOrRequiresRevalidation(crossServiceToken)) {
const authResponse = await this.serviceProxy.validateSession(authHeaderValue) const authResponse = await this.serviceProxy.validateSession({
authorization: authHeaderValue,
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
})
if (!this.handleSessionValidationResponse(authResponse, response, next)) { if (!this.handleSessionValidationResponse(authResponse, response, next)) {
return return
@@ -48,12 +55,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.authToken = crossServiceToken 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) { if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({ await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue, key: cacheKey,
encodedCrossServiceToken: crossServiceToken, encodedCrossServiceToken: response.locals.authToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken), expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid, userUuid: decodedToken.user.uuid,
}) })
@@ -62,6 +71,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user response.locals.user = decodedToken.user
response.locals.session = decodedToken.session response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles response.locals.roles = decodedToken.roles
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
} catch (error) { } catch (error) {
const errorMessage = (error as AxiosError).isAxiosError const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data) ? JSON.stringify((error as AxiosError).response?.data)
@@ -118,4 +128,14 @@ export abstract class AuthMiddleware extends BaseMiddleware {
return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration) 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') @httpGet('/:uuid')
async getItem(request: Request, response: Response): Promise<void> { async getItem(request: Request, response: Response): Promise<void> {
await this.serviceProxy.callSyncingServer( 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) @httpGet('/:userId/params', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)
async getKeyParams(request: Request, response: Response): Promise<void> { async getKeyParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer( await this.httpService.callAuthServer(
@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
constructor(private timer: TimerInterface) {} constructor(private timer: TimerInterface) {}
async set(dto: { async set(dto: {
authorizationHeaderValue: string key: string
encodedCrossServiceToken: string encodedCrossServiceToken: string
expiresAtInSeconds: number expiresAtInSeconds: number
userUuid: string userUuid: string
}): Promise<void> { }): Promise<void> {
let userAuthHeaders = [] let userKeys = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`) const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userAuthHeadersJSON) { if (userKeysJSON) {
userAuthHeaders = JSON.parse(userAuthHeadersJSON) 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.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken) this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds) 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() this.invalidateExpiredTokens()
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`) const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
if (!cachedToken) { if (!cachedToken) {
return null return null
} }
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
} }
async invalidate(userUuid: string): Promise<void> { async invalidate(userUuid: string): Promise<void> {
let userAuthorizationHeaderValues = [] let userKeyValues = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`) const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userAuthHeadersJSON) { if (userKeysJSON) {
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON) userKeyValues = JSON.parse(userKeysJSON)
} }
for (const authorizationHeaderValue of userAuthorizationHeaderValues) { for (const key of userKeyValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`) this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`) this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
} }
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`) this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
this.crossServiceTokenTTLCache.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) {} constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
async set(dto: { async set(dto: {
authorizationHeaderValue: string key: string
encodedCrossServiceToken: string encodedCrossServiceToken: string
expiresAtInSeconds: number expiresAtInSeconds: number
userUuid: string userUuid: string
}): Promise<void> { }): Promise<void> {
const pipeline = this.redisClient.pipeline() 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.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken) pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds) pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
await pipeline.exec() await pipeline.exec()
} }
async get(authorizationHeaderValue: string): Promise<string | null> { async get(key: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`) return this.redisClient.get(`${this.PREFIX}:${key}`)
} }
async invalidate(userUuid: string): Promise<void> { 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() const pipeline = this.redisClient.pipeline()
for (const authorizationHeaderValue of userAuthorizationHeaderValues) { for (const key of userKeyValues) {
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`) pipeline.del(`${this.PREFIX}:${key}`)
} }
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`) pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
@@ -1,10 +1,10 @@
export interface CrossServiceTokenCacheInterface { export interface CrossServiceTokenCacheInterface {
set(dto: { set(dto: {
authorizationHeaderValue: string key: string
encodedCrossServiceToken: string encodedCrossServiceToken: string
expiresAtInSeconds: number expiresAtInSeconds: number
userUuid: string userUuid: string
}): Promise<void> }): Promise<void>
get(authorizationHeaderValue: string): Promise<string | null> get(key: string): Promise<string | null>
invalidate(userUuid: string): Promise<void> invalidate(userUuid: string): Promise<void>
} }
@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
@inject(TYPES.ApiGateway_Logger) private logger: Logger, @inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {} ) {}
async validateSession( async validateSession(headers: {
authorizationHeaderValue: string, authorization: string
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> { sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authResponse = await this.httpClient.request({ const authResponse = await this.httpClient.request({
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: authorizationHeaderValue, Authorization: headers.authorization,
Accept: 'application/json', Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
}, },
validateStatus: (status: number) => { validateStatus: (status: number) => {
return status >= 200 && status < 500 return status >= 200 && status < 500
@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
endpointOrMethodIdentifier: string, endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
): Promise<void> ): Promise<void>
validateSession(authorizationHeaderValue: string): Promise<{ validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
status: number status: number
data: unknown data: unknown
headers: { headers: {
@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
export class DirectCallServiceProxy implements ServiceProxyInterface { export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {} constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
async validateSession( async validateSession(headers: {
authorizationHeaderValue: string, authorization: string
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> { sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue()) const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
if (!authService) { if (!authService) {
throw new Error('Auth service not found') throw new Error('Auth service not found')
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
const serviceResponse = (await authService.handleRequest( const serviceResponse = (await authService.handleRequest(
{ {
headers: { headers: {
authorization: authorizationHeaderValue, authorization: headers.authorization,
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
}, },
} as never, } as never,
{} as never, {} as never,
@@ -42,7 +42,8 @@ export class EndpointResolver implements EndpointResolverInterface {
// Users Controller // Users Controller
['[PATCH]:users/:userId', 'auth.users.update'], ['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'], ['[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'], ['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'], ['[POST]:listed', 'auth.users.createListedAccount'],
['[POST]:auth', 'auth.users.register'], ['[POST]:auth', 'auth.users.register'],
@@ -58,6 +59,7 @@ export class EndpointResolver implements EndpointResolverInterface {
// Syncing Server // Syncing Server
['[POST]:items/sync', 'sync.items.sync'], ['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'], ['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[POST]:items/transition', 'sync.items.transition'],
['[GET]:items/:uuid', 'sync.items.get_item'], ['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2 // Revisions Controller V2
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'], ['[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 LOG_LEVEL=debug
NODE_ENV=development NODE_ENV=development
VERSION=development VERSION=development
+54
View File
@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.135.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.1...@standardnotes/auth-server@1.135.2) (2023-08-28)
### Bug Fixes
* allow self hosted to use new model of items ([#714](https://github.com/standardnotes/server/issues/714)) ([aef9254](https://github.com/standardnotes/server/commit/aef9254713560c00a90a3e84e3cd94417e8f30d2))
## [1.135.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.135.0...@standardnotes/auth-server@1.135.1) (2023-08-25)
### Bug Fixes
* **auth:** account enumeration with pseudo u2f and mfa ([#709](https://github.com/standardnotes/server/issues/709)) ([bbb35d1](https://github.com/standardnotes/server/commit/bbb35d16fc4f6a57fe774a648fbda13ec64a8865))
# [1.135.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.134.0...@standardnotes/auth-server@1.135.0) (2023-08-24)
### Features
* add trigerring items transition and checking status of it ([#707](https://github.com/standardnotes/server/issues/707)) ([05bb12c](https://github.com/standardnotes/server/commit/05bb12c97899824f06e6d01d105dec75fc328440))
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
### Bug Fixes
* **auth:** passing the invalidate cache header ([#697](https://github.com/standardnotes/server/issues/697)) ([83ad069](https://github.com/standardnotes/server/commit/83ad069c5dd9afa3a6db881f0d8a55a58d0642aa))
## [1.131.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.3...@standardnotes/auth-server@1.131.4) (2023-08-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.2...@standardnotes/auth-server@1.131.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.1...@standardnotes/auth-server@1.131.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.0...@standardnotes/auth-server@1.131.1) (2023-08-09) ## [1.131.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.0...@standardnotes/auth-server@1.131.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server **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", "name": "@standardnotes/auth-server",
"version": "1.131.1", "version": "1.135.2",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "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 { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler' import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler' 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 { export class ContainerConfigLoader {
async load(configuration?: { async load(configuration?: {
@@ -560,6 +567,9 @@ export class ContainerConfigLoader {
container container
.bind(TYPES.Auth_READONLY_USERS) .bind(TYPES.Auth_READONLY_USERS)
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : []) .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) { if (isConfiguredForInMemoryCache) {
container container
@@ -606,6 +616,9 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Timer), container.get(TYPES.Auth_Timer),
), ),
) )
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new InMemoryTransitionStatusRepository())
} else { } else {
container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository) container.bind<PKCERepositoryInterface>(TYPES.Auth_PKCERepository).to(RedisPKCERepository)
container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository) container.bind<LockRepositoryInterface>(TYPES.Auth_LockRepository).to(LockRepository)
@@ -618,6 +631,9 @@ export class ContainerConfigLoader {
container container
.bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository) .bind<SubscriptionTokenRepositoryInterface>(TYPES.Auth_SubscriptionTokenRepository)
.to(RedisSubscriptionTokenRepository) .to(RedisSubscriptionTokenRepository)
container
.bind<TransitionStatusRepositoryInterface>(TYPES.Auth_TransitionStatusRepository)
.toConstantValue(new RedisTransitionStatusRepository(container.get<Redis>(TYPES.Auth_Redis)))
} }
// Services // Services
@@ -894,6 +910,22 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_SubscriptionSettingService), 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 // Controller
container container
@@ -979,6 +1011,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger), 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 container
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler) .bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
.toConstantValue( .toConstantValue(
@@ -1027,6 +1067,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger), 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([ const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)], ['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
@@ -1042,6 +1090,7 @@ export class ContainerConfigLoader {
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)], ['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)], ['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)], ['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)], ['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)], ['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)], ['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
@@ -1057,6 +1106,7 @@ export class ContainerConfigLoader {
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)], ['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)], ['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)], ['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
]) ])
if (isConfiguredForHomeServer) { if (isConfiguredForHomeServer) {
@@ -1161,14 +1211,15 @@ export class ContainerConfigLoader {
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController) .bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue( .toConstantValue(
new BaseUsersController( new BaseUsersController(
container.get(TYPES.Auth_UpdateUser), container.get<UpdateUser>(TYPES.Auth_UpdateUser),
container.get(TYPES.Auth_GetUserKeyParams), container.get<GetUserKeyParams>(TYPES.Auth_GetUserKeyParams),
container.get(TYPES.Auth_DeleteAccount), container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
container.get(TYPES.Auth_GetUserSubscription), container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
container.get(TYPES.Auth_ClearLoginAttempts), container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
container.get(TYPES.Auth_IncreaseLoginAttempts), container.get<IncreaseLoginAttempts>(TYPES.Auth_IncreaseLoginAttempts),
container.get(TYPES.Auth_ChangeCredentials), container.get<ChangeCredentials>(TYPES.Auth_ChangeCredentials),
container.get(TYPES.Auth_ControllerContainer), container.get<GetTransitionStatus>(TYPES.Auth_GetTransitionStatus),
container.get<ControllerContainerInterface>(TYPES.Auth_ControllerContainer),
), ),
) )
container container
+1
View File
@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
async activatePremiumFeatures(dto: { async activatePremiumFeatures(dto: {
username: string username: string
subscriptionPlanName?: string subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date endsAt?: Date
}): Promise<Result<string>> { }): Promise<Result<string>> {
if (!this.container) { if (!this.container) {
+6
View File
@@ -35,6 +35,7 @@ const TYPES = {
Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'), Auth_AuthenticatorRepository: Symbol.for('Auth_AuthenticatorRepository'),
Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'), Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'), Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
// ORM // ORM
Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'), Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'), Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'),
@@ -101,6 +102,7 @@ const TYPES = {
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'), Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'), Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'), Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
// use cases // use cases
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'), Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'), Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
@@ -153,6 +155,8 @@ const TYPES = {
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'), Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'), Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'), Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
// Handlers // Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'), Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'), Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -167,6 +171,7 @@ const TYPES = {
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'), Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'), Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'), Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'), Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'), Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'), Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
@@ -180,6 +185,7 @@ const TYPES = {
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'), Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'), Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'), Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
// Services // Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'), Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'), 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() offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn() roleService.removeUserRoleBasedOnSubscription = jest.fn()
timestamp = dayjs.utc().valueOf() timestamp = dayjs.utc().valueOf()
@@ -86,7 +86,7 @@ describe('SubscriptionExpiredEventHandler', () => {
it('should update the user role', async () => { it('should update the user role', async () => {
await createHandler().handle(event) 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 () => { it('should update subscription ends at', async () => {
@@ -108,7 +108,7 @@ describe('SubscriptionExpiredEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled() expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled() expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
}) })
@@ -117,7 +117,7 @@ describe('SubscriptionExpiredEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled() expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).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> { private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId) const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) { 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) offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn() roleService.setOfflineUserRole = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000 subscriptionExpiresAt = timestamp + 365 * 1000
@@ -106,7 +106,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
it('should update the user role', async () => { it('should update the user role', async () => {
await createHandler().handle(event) 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 () => { it('should update user default settings', async () => {
@@ -162,7 +162,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
@@ -171,7 +171,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).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> { private async addUserRole(user: User, subscriptionName: string): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName) await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
} }
private async createSubscription( private async createSubscription(
@@ -62,7 +62,7 @@ describe('SubscriptionReassignedEventHandler', () => {
userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription) userSubscriptionRepository.save = jest.fn().mockReturnValue(subscription)
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000 subscriptionExpiresAt = timestamp + 365 * 1000
@@ -100,7 +100,7 @@ describe('SubscriptionReassignedEventHandler', () => {
it('should update the user role', async () => { it('should update the user role', async () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).toHaveBeenCalledWith(user, SubscriptionName.ProPlan) expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(user, SubscriptionName.ProPlan)
}) })
it('should create subscription', async () => { it('should create subscription', async () => {
@@ -146,7 +146,7 @@ describe('SubscriptionReassignedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
@@ -155,7 +155,7 @@ describe('SubscriptionReassignedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).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> { private async addUserRole(user: User, subscriptionName: string): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName) await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
} }
private async createSubscription( private async createSubscription(
@@ -61,7 +61,7 @@ describe('SubscriptionRefundedEventHandler', () => {
offlineUserSubscriptionRepository.updateEndsAt = jest.fn() offlineUserSubscriptionRepository.updateEndsAt = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn() roleService.removeUserRoleBasedOnSubscription = jest.fn()
timestamp = dayjs.utc().valueOf() timestamp = dayjs.utc().valueOf()
@@ -87,7 +87,7 @@ describe('SubscriptionRefundedEventHandler', () => {
it('should update the user role', async () => { it('should update the user role', async () => {
await createHandler().handle(event) 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 () => { it('should update subscription ends at', async () => {
@@ -109,7 +109,7 @@ describe('SubscriptionRefundedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled() expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled() expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
}) })
@@ -118,7 +118,7 @@ describe('SubscriptionRefundedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.removeUserRole).not.toHaveBeenCalled() expect(roleService.removeUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.updateEndsAt).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> { private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId) const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) { 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) offlineUserSubscriptionRepository.save = jest.fn().mockReturnValue(offlineUserSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn() roleService.setOfflineUserRole = jest.fn()
timestamp = dayjs.utc().valueOf() timestamp = dayjs.utc().valueOf()
@@ -107,7 +107,7 @@ describe('SubscriptionRenewedEventHandler', () => {
it('should update the user role', async () => { it('should update the user role', async () => {
await createHandler().handle(event) 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 () => { it('should update the offline user role', async () => {
@@ -123,7 +123,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
@@ -132,7 +132,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
@@ -143,7 +143,7 @@ describe('SubscriptionRenewedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
}) })
@@ -73,7 +73,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
for (const userSubscription of userSubscriptions) { for (const userSubscription of userSubscriptions) {
const user = await userSubscription.user 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 = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
roleService.setOfflineUserRole = jest.fn() roleService.setOfflineUserRole = jest.fn()
subscriptionExpiresAt = timestamp + 365 * 1000 subscriptionExpiresAt = timestamp + 365 * 1000
@@ -121,7 +121,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
it('should update the user role', async () => { it('should update the user role', async () => {
await createHandler().handle(event) 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 () => { it('should update user default settings', async () => {
@@ -243,7 +243,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
@@ -252,7 +252,7 @@ describe('SubscriptionSyncRequestedEventHandler', () => {
await createHandler().handle(event) await createHandler().handle(event)
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
}) })
}) })
@@ -93,7 +93,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp, event.payload.timestamp,
) )
await this.roleService.addUserRole(user, event.payload.subscriptionName) await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(userSubscription) 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 { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface' import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { SubscriptionName } from '@standardnotes/common' import { SubscriptionName } from '@standardnotes/common'
import { RoleName } from '@standardnotes/domain-core' import { RoleName, Uuid } from '@standardnotes/domain-core'
import { Role } from '../Role/Role' import { Role } from '../Role/Role'
import { ClientServiceInterface } from '../Client/ClientServiceInterface' import { ClientServiceInterface } from '../Client/ClientServiceInterface'
@@ -81,9 +81,44 @@ describe('RoleService', () => {
logger = {} as jest.Mocked<Logger> logger = {} as jest.Mocked<Logger>
logger.info = jest.fn() logger.info = jest.fn()
logger.warn = jest.fn() logger.warn = jest.fn()
logger.error = jest.fn()
}) })
describe('adding roles', () => { 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(() => { beforeEach(() => {
user = { user = {
uuid: '123', uuid: '123',
@@ -96,7 +131,7 @@ describe('RoleService', () => {
}) })
it('should add role to user', async () => { 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) expect(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
user.roles = Promise.resolve([basicRole, proRole]) user.roles = Promise.resolve([basicRole, proRole])
@@ -112,7 +147,7 @@ describe('RoleService', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user) 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(roleRepository.findOneByName).toHaveBeenCalledWith(RoleName.NAMES.ProUser)
expect(userRepository.save).toHaveBeenCalledWith(user) expect(userRepository.save).toHaveBeenCalledWith(user)
@@ -120,7 +155,7 @@ describe('RoleService', () => {
}) })
it('should send websockets event', async () => { it('should send websockets event', async () => {
await createService().addUserRole(user, SubscriptionName.ProPlan) await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user) 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 () => { it('should not add role if no role name exists for subscription name', async () => {
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined) 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() expect(userRepository.save).not.toHaveBeenCalled()
}) })
it('should not add role if no role exists for role name', async () => { it('should not add role if no role exists for role name', async () => {
roleRepository.findOneByName = jest.fn().mockReturnValue(null) roleRepository.findOneByName = jest.fn().mockReturnValue(null)
await createService().addUserRole(user, SubscriptionName.ProPlan) await createService().addUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(userRepository.save).not.toHaveBeenCalled() expect(userRepository.save).not.toHaveBeenCalled()
}) })
@@ -169,7 +204,7 @@ describe('RoleService', () => {
}) })
}) })
describe('removing roles', () => { describe('removing roles based on subscription', () => {
beforeEach(() => { beforeEach(() => {
user = { user = {
uuid: '123', uuid: '123',
@@ -182,13 +217,13 @@ describe('RoleService', () => {
}) })
it('should remove role from user', async () => { it('should remove role from user', async () => {
await createService().removeUserRole(user, SubscriptionName.ProPlan) await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(userRepository.save).toHaveBeenCalledWith(user) expect(userRepository.save).toHaveBeenCalledWith(user)
}) })
it('should send websockets event', async () => { it('should send websockets event', async () => {
await createService().removeUserRole(user, SubscriptionName.ProPlan) await createService().removeUserRoleBasedOnSubscription(user, SubscriptionName.ProPlan)
expect(webSocketsClientService.sendUserRolesChangedEvent).toHaveBeenCalledWith(user) 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 () => { it('should not remove role if role name does not exist for subscription name', async () => {
roleToSubscriptionMap.getRoleNameForSubscriptionName = jest.fn().mockReturnValue(undefined) 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() expect(userRepository.save).not.toHaveBeenCalled()
}) })
+38 -22
View File
@@ -13,7 +13,7 @@ import { RoleToSubscriptionMapInterface } from './RoleToSubscriptionMapInterface
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface' import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Role } from './Role' import { Role } from './Role'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { Uuid } from '@standardnotes/domain-core' import { RoleName, Uuid } from '@standardnotes/domain-core'
@injectable() @injectable()
export class RoleService implements RoleServiceInterface { export class RoleService implements RoleServiceInterface {
@@ -54,7 +54,18 @@ export class RoleService implements RoleServiceInterface {
return false 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) const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
if (roleName === undefined) { if (roleName === undefined) {
@@ -62,25 +73,7 @@ export class RoleService implements RoleServiceInterface {
return return
} }
const role = await this.roleRepository.findOneByName(roleName) await this.addToExistingRoles(user, 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)
} }
async setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void> { async setOfflineUserRole(offlineUserSubscription: OfflineUserSubscription): Promise<void> {
@@ -107,7 +100,7 @@ export class RoleService implements RoleServiceInterface {
await this.offlineUserSubscriptionRepository.save(offlineUserSubscription) 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) const roleName = this.roleToSubscriptionMap.getRoleNameForSubscriptionName(subscriptionName)
if (roleName === undefined) { if (roleName === undefined) {
@@ -120,4 +113,27 @@ export class RoleService implements RoleServiceInterface {
await this.userRepository.save(user) await this.userRepository.save(user)
await this.webSocketsClientService.sendUserRolesChangedEvent(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 { PermissionName } from '@standardnotes/features'
import { RoleName, Uuid } from '@standardnotes/domain-core'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription' import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { User } from '../User/User' import { User } from '../User/User'
export interface RoleServiceInterface { 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> 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> 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) userSubscriptionRepository.save = jest.fn().mockReturnValue(inviteeSubscription)
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface> subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn() subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
@@ -103,7 +103,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
updatedAt: 1, updatedAt: 1,
user: Promise.resolve(invitee), user: Promise.resolve(invitee),
}) })
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN') expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription, inviteeSubscription,
) )
@@ -143,7 +143,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
updatedAt: 3, updatedAt: 3,
user: Promise.resolve(invitee), user: Promise.resolve(invitee),
}) })
expect(roleService.addUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN') expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith( expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).toHaveBeenCalledWith(
inviteeSubscription, inviteeSubscription,
) )
@@ -162,7 +162,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
}) })
@@ -180,7 +180,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
}) })
@@ -202,7 +202,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
}) })
@@ -219,7 +219,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled() expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).not.toHaveBeenCalled()
}) })
@@ -244,7 +244,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled() expect(sharedSubscriptionInvitationRepository.save).not.toHaveBeenCalled()
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() expect(userSubscriptionRepository.save).not.toHaveBeenCalled()
expect(roleService.addUserRole).not.toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).not.toHaveBeenCalled()
expect(subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription).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> { private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
await this.roleService.addUserRole(user, subscriptionName) await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
} }
private async createSharedSubscription( private async createSharedSubscription(
@@ -34,7 +34,7 @@ describe('ActivatePremiumFeatures', () => {
userSubscriptionRepository.save = jest.fn() userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
timer = {} as jest.Mocked<TimerInterface> timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789) timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
@@ -73,7 +73,7 @@ describe('ActivatePremiumFeatures', () => {
expect(result.isFailed()).toBe(false) expect(result.isFailed()).toBe(false)
expect(userSubscriptionRepository.save).toHaveBeenCalled() expect(userSubscriptionRepository.save).toHaveBeenCalled()
expect(roleService.addUserRole).toHaveBeenCalled() expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalled()
}) })
it('should save a subscription with custom plan name and endsAt', async () => { 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.userSubscriptionRepository.save(subscription)
await this.roleService.addUserRole(user, subscriptionPlanName.value) await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionPlanName.value)
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription( await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
subscription, subscription,
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]), new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
) )
return Result.ok('Premium features activated.') return Result.ok('Premium features activated.')
@@ -1,5 +1,6 @@
export interface ActivatePremiumFeaturesDTO { export interface ActivatePremiumFeaturesDTO {
username: string username: string
subscriptionPlanName?: string subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date endsAt?: Date
} }
@@ -84,7 +84,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
userSubscriptionRepository.save = jest.fn() userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.removeUserRole = jest.fn() roleService.removeUserRoleBasedOnSubscription = jest.fn()
timer = {} as jest.Mocked<TimerInterface> timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1) timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
@@ -122,7 +122,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
endsAt: 1, endsAt: 1,
user: Promise.resolve(invitee), user: Promise.resolve(invitee),
}) })
expect(roleService.removeUserRole).toHaveBeenCalledWith(invitee, 'PLUS_PLAN') expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(domainEventPublisher.publish).toHaveBeenCalled() expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createSharedSubscriptionInvitationCanceledEvent).toHaveBeenCalledWith({ expect(domainEventFactory.createSharedSubscriptionInvitationCanceledEvent).toHaveBeenCalledWith({
inviteeIdentifier: '123', inviteeIdentifier: '123',
@@ -156,7 +156,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: 'email', inviteeIdentifierType: 'email',
}) })
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() 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 () => { it('should not cancel a shared subscription invitation if it is not found', async () => {
@@ -204,7 +204,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
inviteeIdentifierType: 'email', inviteeIdentifierType: 'email',
}) })
expect(userSubscriptionRepository.save).not.toHaveBeenCalled() 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 () => { 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) { if (invitee !== null) {
await this.removeSharedSubscription(sharedSubscriptionInvitation.subscriptionId, invitee) 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( await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({ this.domainEventFactory.createSharedSubscriptionInvitationCanceledEvent({
@@ -8,6 +8,9 @@ import { Role } from '../../Role/Role'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken' import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { GetSetting } from '../GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
describe('CreateCrossServiceToken', () => { describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User> let userProjector: ProjectorInterface<User>
@@ -15,6 +18,8 @@ describe('CreateCrossServiceToken', () => {
let roleProjector: ProjectorInterface<Role> let roleProjector: ProjectorInterface<Role>
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData> let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface let userRepository: UserRepositoryInterface
let getSettingUseCase: GetSetting
let transitionStatusRepository: TransitionStatusRepositoryInterface
const jwtTTL = 60 const jwtTTL = 60
let session: Session let session: Session
@@ -22,7 +27,16 @@ describe('CreateCrossServiceToken', () => {
let role: Role let role: Role
const createUseCase = () => const createUseCase = () =>
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL) new CreateCrossServiceToken(
userProjector,
sessionProjector,
roleProjector,
tokenEncoder,
userRepository,
jwtTTL,
getSettingUseCase,
transitionStatusRepository,
)
beforeEach(() => { beforeEach(() => {
session = {} as jest.Mocked<Session> session = {} as jest.Mocked<Session>
@@ -50,6 +64,12 @@ describe('CreateCrossServiceToken', () => {
userRepository = {} as jest.Mocked<UserRepositoryInterface> userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user) 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 () => { it('should create a cross service token for user', async () => {
@@ -73,6 +93,36 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te', email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000', 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, 60,
) )
@@ -95,6 +145,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te', email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000', uuid: '00000000-0000-0000-0000-000000000000',
}, },
ongoing_transition: false,
}, },
60, 60,
) )
@@ -117,6 +168,7 @@ describe('CreateCrossServiceToken', () => {
email: 'test@test.te', email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000', uuid: '00000000-0000-0000-0000-000000000000',
}, },
ongoing_transition: false,
}, },
60, 60,
) )
@@ -125,28 +177,75 @@ describe('CreateCrossServiceToken', () => {
it('should throw an error if user does not exist', async () => { it('should throw an error if user does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null) userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null const result = await createUseCase().execute({
try { userUuid: '00000000-0000-0000-0000-000000000000',
await createUseCase().execute({ })
userUuid: '00000000-0000-0000-0000-000000000000',
})
} catch (error) {
caughtError = error
}
expect(caughtError).not.toBeNull() expect(result.isFailed()).toBeTruthy()
}) })
it('should throw an error if user uuid is invalid', async () => { it('should throw an error if user uuid is invalid', async () => {
let caughtError = null const result = await createUseCase().execute({
try { userUuid: 'invalid',
await createUseCase().execute({ })
userUuid: 'invalid',
})
} catch (error) {
caughtError = error
}
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 { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
@@ -7,14 +8,14 @@ import { Role } from '../../Role/Role'
import { Session } from '../../Session/Session' import { Session } from '../../Session/Session'
import { User } from '../../User/User' import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO' import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse' import { GetSetting } from '../GetSetting/GetSetting'
import { Uuid } from '@standardnotes/domain-core' import { SettingName } from '@standardnotes/settings'
import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
@injectable() @injectable()
export class CreateCrossServiceToken implements UseCaseInterface { export class CreateCrossServiceToken implements UseCaseInterface<string> {
constructor( constructor(
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>, @inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>, @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_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface, @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number, @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 let user: User | undefined | null = dto.user
if (user === undefined && dto.userUuid !== undefined) { if (user === undefined && dto.userUuid !== undefined) {
const userUuidOrError = Uuid.create(dto.userUuid) const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) { if (userUuidOrError.isFailed()) {
throw new Error(userUuidOrError.getError()) return Result.fail(userUuidOrError.getError())
} }
const userUuid = userUuidOrError.getValue() const userUuid = userUuidOrError.getValue()
@@ -37,23 +42,44 @@ export class CreateCrossServiceToken implements UseCaseInterface {
} }
if (!user) { 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 roles = await user.roles
const authTokenData: CrossServiceTokenData = { const authTokenData: CrossServiceTokenData = {
user: this.projectUser(user), user: this.projectUser(user),
roles: this.projectRoles(roles), 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) { if (dto.session !== undefined) {
authTokenData.session = this.projectSession(dto.session) authTokenData.session = this.projectSession(dto.session)
} }
return { return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
}
} }
private projectUser(user: User): { uuid: string; email: string } { private projectUser(user: User): { uuid: string; email: string } {
@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
{ {
user: User user: User
session?: Session session?: Session
sharedVaultOwnerContext?: string
}, },
{ {
userUuid: string userUuid: string
@@ -1,3 +0,0 @@
export type CreateCrossServiceTokenResponse = {
token: string
}
@@ -73,35 +73,30 @@ describe('GetSetting', () => {
describe('no subscription', () => { describe('no subscription', () => {
it('should find a setting for user', async () => { it('should find a setting for user', async () => {
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.DropboxBackupFrequency,
success: true, })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3', userUuid: '1-2-3',
setting: { foo: 'bar' }, setting: { foo: 'bar' },
}) })
}) })
it('should not find a setting if the setting name is invalid', async () => { it('should not find a setting if the setting name is invalid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({ const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
success: false, expect(result.isFailed()).toBeTruthy()
error: {
message: 'Invalid setting name: invalid',
},
})
}) })
it('should not get a setting for user if it does not exist', async () => { it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null) settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.DropboxBackupFrequency,
success: false,
error: {
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
},
}) })
expect(result.isFailed()).toBeTruthy()
}) })
it('should not retrieve a sensitive setting for user', async () => { it('should not retrieve a sensitive setting for user', async () => {
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting) settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({ const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
success: true, expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true, sensitive: true,
}) })
}) })
it('should not retrieve a subscription setting for user', async () => { it('should not retrieve a subscription setting for user', async () => {
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.MuteSignInEmails,
success: false,
error: {
message: 'No subscription found.',
},
}) })
expect(result.isFailed()).toBeTruthy()
}) })
it('should retrieve a sensitive setting for user if explicitly told to', async () => { 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) settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3',
userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret,
settingName: SettingName.NAMES.MfaSecret, allowSensitiveRetrieval: true,
allowSensitiveRetrieval: true, })
}), expect(result.isFailed()).toBeFalsy()
).toEqual({ expect(result.getValue()).toEqual({
success: true,
userUuid: '1-2-3', userUuid: '1-2-3',
setting: { foo: 'bar' }, setting: { foo: 'bar' },
}) })
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
}) })
it('should find a setting for user', async () => { it('should find a setting for user', async () => {
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.MuteSignInEmails,
success: true, })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3', userUuid: '1-2-3',
setting: { foo: 'sub-bar' }, 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 () => { it('should not get a suscription setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null) subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.MuteSignInEmails,
success: false,
error: {
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
},
}) })
expect(result.isFailed()).toBeTruthy()
}) })
it('should not retrieve a sensitive subscription setting for user', async () => { it('should not retrieve a sensitive subscription setting for user', async () => {
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
.fn() .fn()
.mockReturnValue(subscriptionSetting) .mockReturnValue(subscriptionSetting)
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.MuteSignInEmails,
success: true, })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true, sensitive: true,
}) })
}) })
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
}) })
it('should find a setting for user', async () => { it('should find a setting for user', async () => {
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.MuteSignInEmails,
success: true, })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3', userUuid: '1-2-3',
setting: { foo: 'sub-bar' }, setting: { foo: 'sub-bar' },
}) })
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
}) })
it('should find a regular subscription only setting for user', async () => { it('should find a regular subscription only setting for user', async () => {
expect( const result = await createUseCase().execute({
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }), userUuid: '1-2-3',
).toEqual({ settingName: SettingName.NAMES.FileUploadBytesLimit,
success: true, })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3', userUuid: '1-2-3',
setting: { foo: 'sub-bar' }, setting: { foo: 'sub-bar' },
}) })
@@ -1,7 +1,7 @@
import { SettingName } from '@standardnotes/settings' import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
import { SettingProjector } from '../../../Projection/SettingProjector' import { SettingProjector } from '../../../Projection/SettingProjector'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
import { UserSubscription } from '../../Subscription/UserSubscription' import { UserSubscription } from '../../Subscription/UserSubscription'
@injectable() @injectable()
export class GetSetting implements UseCaseInterface { export class GetSetting implements UseCaseInterface<GetSettingResponse> {
constructor( constructor(
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector, @inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector, @inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface, @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) const settingNameOrError = SettingName.create(dto.settingName)
if (settingNameOrError.isFailed()) { if (settingNameOrError.isFailed()) {
return { return Result.fail(settingNameOrError.getError())
success: false,
error: {
message: settingNameOrError.getError(),
},
}
} }
const settingName = settingNameOrError.getValue() const settingName = settingNameOrError.getValue()
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
} }
if (!subscription) { if (!subscription) {
return { return Result.fail('No subscription found.')
success: false,
error: {
message: 'No subscription found.',
},
}
} }
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
}) })
if (subscriptionSetting === null) { if (subscriptionSetting === null) {
return { return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
success: false,
error: {
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
} }
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) { if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
return { return Result.ok({
success: true,
sensitive: true, sensitive: true,
} })
} }
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting) const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
return { return Result.ok({
success: true,
userUuid: dto.userUuid, userUuid: dto.userUuid,
setting: simpleSubscriptionSetting, setting: simpleSubscriptionSetting,
} })
} }
const setting = await this.settingService.findSettingWithDecryptedValue({ const setting = await this.settingService.findSettingWithDecryptedValue({
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
}) })
if (setting === null) { if (setting === null) {
return { return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
success: false,
error: {
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
} }
if (setting.sensitive && !dto.allowSensitiveRetrieval) { if (setting.sensitive && !dto.allowSensitiveRetrieval) {
return { return Result.ok({
success: true,
sensitive: true, sensitive: true,
} })
} }
const simpleSetting = await this.settingProjector.projectSimple(setting) const simpleSetting = await this.settingProjector.projectSimple(setting)
return { return Result.ok({
success: true,
userUuid: dto.userUuid, userUuid: dto.userUuid,
setting: simpleSetting, setting: simpleSetting,
} })
} }
} }
@@ -1,18 +1,13 @@
import { Either } from '@standardnotes/common'
import { SimpleSetting } from '../../Setting/SimpleSetting' import { SimpleSetting } from '../../Setting/SimpleSetting'
export type GetSettingResponse = export type GetSettingResponse = Either<
| { {
success: true userUuid: string
userUuid: string setting: SimpleSetting
setting: SimpleSetting },
} {
| { sensitive: true
success: true }
sensitive: true >
}
| {
success: false
error: {
message: string
}
}
@@ -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 { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115' import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { Session } from '../Session/Session' import { Session } from '../Session/Session'
import { RoleName } from '@standardnotes/domain-core'
describe('Register', () => { describe('Register', () => {
let userRepository: UserRepositoryInterface let userRepository: UserRepositoryInterface
@@ -20,9 +21,19 @@ describe('Register', () => {
let user: User let user: User
let crypter: CrypterInterface let crypter: CrypterInterface
let timer: TimerInterface let timer: TimerInterface
let transitionModeEnabled = false
const createUseCase = () => const createUseCase = () =>
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer) new Register(
userRepository,
roleRepository,
authResponseFactory,
crypter,
false,
settingService,
timer,
transitionModeEnabled,
)
beforeEach(() => { beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface> userRepository = {} as jest.Mocked<UserRepositoryInterface>
@@ -75,6 +86,7 @@ describe('Register', () => {
updatedWithUserAgent: 'Mozilla', updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String), uuid: expect.any(String),
version: '004', version: '004',
roles: Promise.resolve([]),
createdAt: new Date(1), createdAt: new Date(1),
updatedAt: 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 () => { it('should fail to register if username is invalid', async () => {
expect( expect(
await createUseCase().execute({ await createUseCase().execute({
@@ -195,6 +249,7 @@ describe('Register', () => {
true, true,
settingService, settingService,
timer, timer,
transitionModeEnabled,
).execute({ ).execute({
email: 'test@test.te', email: 'test@test.te',
password: 'asdzxc', password: 'asdzxc',
+12 -3
View File
@@ -1,8 +1,9 @@
import * as bcrypt from 'bcryptjs' import * as bcrypt from 'bcryptjs'
import { RoleName, Username } from '@standardnotes/domain-core' import { RoleName, Username } from '@standardnotes/domain-core'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { inject, injectable } from 'inversify' import { inject, injectable } from 'inversify'
import { TimerInterface } from '@standardnotes/time'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
import { User } from '../User/User' import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface' import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@@ -11,7 +12,6 @@ import { RegisterResponse } from './RegisterResponse'
import { UseCaseInterface } from './UseCaseInterface' import { UseCaseInterface } from './UseCaseInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface' import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface' import { CrypterInterface } from '../Encryption/CrypterInterface'
import { TimerInterface } from '@standardnotes/time'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface' import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115' import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115' 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_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface, @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface, @inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
) {} ) {}
async execute(dto: RegisterDTO): Promise<RegisterResponse> { async execute(dto: RegisterDTO): Promise<RegisterResponse> {
@@ -72,10 +73,18 @@ export class Register implements UseCaseInterface {
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey() user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
const roles = []
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser) const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
if (defaultRole) { 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) Object.assign(user, registrationFields)
@@ -93,7 +93,7 @@ describe('UpdateSetting', () => {
settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true) settingsAssociationService.isSettingMutableByClient = jest.fn().mockReturnValue(true)
roleService = {} as jest.Mocked<RoleServiceInterface> roleService = {} as jest.Mocked<RoleServiceInterface>
roleService.addUserRole = jest.fn() roleService.addUserRoleBasedOnSubscription = jest.fn()
logger = {} as jest.Mocked<Logger> logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn() logger.debug = jest.fn()
@@ -7,7 +7,6 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface' import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO' import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
import { User } from '../../User/User'
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> { export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
constructor( 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}`) 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) { if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed) await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
} }
return Result.ok() return Result.ok()
} }
private async updateUploadBytesUsedSetting( private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
subscription: UserSubscription,
user: User,
bytesUsed: number,
): Promise<void> {
let bytesAlreadyUsed = '0' let bytesAlreadyUsed = '0'
const subscriptionUser = await subscription.user
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({ const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid, userUuid: subscriptionUser.uuid,
userSubscriptionUuid: subscription.uuid, userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
}) })
@@ -60,7 +56,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
await this.subscriptionSettingService.createOrReplace({ await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription, userSubscription: subscription,
user, user: subscriptionUser,
props: { props: {
name: SettingName.NAMES.FileUploadBytesUsed, name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(), 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 () => { 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) userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
expect( expect(
+22 -22
View File
@@ -48,33 +48,33 @@ export class VerifyMFA implements UseCaseInterface {
const user = await this.userRepository.findOneByUsernameOrEmail(username) const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user == null) { if (user == null) {
const mfaSelectorHash = crypto const secondFactorSelectorHash = crypto
.createHash('sha256') .createHash('sha256')
.update(`mfa-selector-${dto.email}${this.pseudoKeyParamsKey}`) .update(`second-factor-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex')
const u2fSelectorHash = crypto
.createHash('sha256')
.update(`u2f-selector-${dto.email}${this.pseudoKeyParamsKey}`)
.digest('hex') .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) { if (isPseudoU2FRequired) {
return { return {
success: false, success: false,
errorTag: ErrorTag.MfaRequired, errorTag: ErrorTag.U2FRequired,
errorMessage: 'Please enter your two-factor authentication code.', errorMessage: 'Please authenticate with your U2F device.',
errorPayload: { mfa_key: `mfa_${uuidv4()}` }, }
} } else {
} return {
success: false,
if (isPseudoU2FRequired) { errorTag: ErrorTag.MfaRequired,
return { errorMessage: 'Please enter your two-factor authentication code.',
success: false, errorPayload: { mfa_key: `mfa_${uuidv4()}` },
errorTag: ErrorTag.U2FRequired, }
errorMessage: 'Please authenticate with your U2F device.',
} }
} }
@@ -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 { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures' import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedInternalController', () => { describe('AnnotatedInternalController', () => {
let getUserFeatures: GetUserFeatures let getUserFeatures: GetUserFeatures
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3' request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar' 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 httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync() const result = await httpResponse.executeAsync()
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3' request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar' 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 httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync() const result = await httpResponse.executeAsync()
@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
@httpGet('/users/:userUuid/settings/:settingName') @httpGet('/users/:userUuid/settings/:settingName')
async getSetting(request: Request): Promise<results.JsonResult> { async getSetting(request: Request): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({ const resultOrError = await this.doGetSetting.execute({
userUuid: request.params.userUuid, userUuid: request.params.userUuid,
settingName: request.params.settingName, settingName: request.params.settingName,
allowSensitiveRetrieval: true, allowSensitiveRetrieval: true,
}) })
if (result.success) { if (resultOrError.isFailed()) {
return this.json(result) 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 { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
import { ProjectorInterface } from '../../Projection/ProjectorInterface' import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Session } from '../../Domain/Session/Session' import { Session } from '../../Domain/Session/Session'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSessionsController', () => { describe('AnnotatedSessionsController', () => {
let getActiveSessionsForUser: GetActiveSessionsForUser let getActiveSessionsForUser: GetActiveSessionsForUser
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' }) sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken> createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' }) createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
request = { request = {
params: {}, params: {},
@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings' import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting' import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
import { User } from '../../Domain/User/User' import { User } from '../../Domain/User/User'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSettingsController', () => { describe('AnnotatedSettingsController', () => {
let deleteSetting: DeleteSetting let deleteSetting: DeleteSetting
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3', 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 httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync() const result = await httpResponse.executeAsync()
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3', 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 httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync() const result = await httpResponse.executeAsync()
@@ -6,6 +6,7 @@ import { results } from 'inversify-express-utils'
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController' import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
import { User } from '../../Domain/User/User' import { User } from '../../Domain/User/User'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting' import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSubscriptionSettingsController', () => { describe('AnnotatedSubscriptionSettingsController', () => {
let getSetting: GetSetting let getSetting: GetSetting
@@ -41,7 +42,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3', 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 httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync() const result = await httpResponse.executeAsync()
@@ -58,7 +59,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3', 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 httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync() const result = await httpResponse.executeAsync()
@@ -14,6 +14,7 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription' import { InviteToSharedSubscription } from '../../Domain/UseCase/InviteToSharedSubscription/InviteToSharedSubscription'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser' import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { User } from '../../Domain/User/User' import { User } from '../../Domain/User/User'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
describe('AnnotatedUsersController', () => { describe('AnnotatedUsersController', () => {
let updateUser: UpdateUser let updateUser: UpdateUser
@@ -24,6 +25,7 @@ describe('AnnotatedUsersController', () => {
let increaseLoginAttempts: IncreaseLoginAttempts let increaseLoginAttempts: IncreaseLoginAttempts
let changeCredentials: ChangeCredentials let changeCredentials: ChangeCredentials
let inviteToSharedSubscription: InviteToSharedSubscription let inviteToSharedSubscription: InviteToSharedSubscription
let getTransitionStatus: GetTransitionStatus
let request: express.Request let request: express.Request
let response: express.Response let response: express.Response
@@ -38,6 +40,7 @@ describe('AnnotatedUsersController', () => {
clearLoginAttempts, clearLoginAttempts,
increaseLoginAttempts, increaseLoginAttempts,
changeCredentials, changeCredentials,
getTransitionStatus,
) )
beforeEach(() => { beforeEach(() => {
@@ -69,6 +72,9 @@ describe('AnnotatedUsersController', () => {
inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription> inviteToSharedSubscription = {} as jest.Mocked<InviteToSharedSubscription>
inviteToSharedSubscription.execute = jest.fn() inviteToSharedSubscription.execute = jest.fn()
getTransitionStatus = {} as jest.Mocked<GetTransitionStatus>
getTransitionStatus.execute = jest.fn()
request = { request = {
headers: {}, headers: {},
body: {}, body: {},
@@ -18,6 +18,7 @@ import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts' import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials' import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { BaseUsersController } from './Base/BaseUsersController' import { BaseUsersController } from './Base/BaseUsersController'
import { GetTransitionStatus } from '../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
@controller('/users') @controller('/users')
export class AnnotatedUsersController extends BaseUsersController { export class AnnotatedUsersController extends BaseUsersController {
@@ -29,6 +30,7 @@ export class AnnotatedUsersController extends BaseUsersController {
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts, @inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts, @inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials, @inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
@inject(TYPES.Auth_GetTransitionStatus) override getTransitionStatusUseCase: GetTransitionStatus,
) { ) {
super( super(
updateUser, updateUser,
@@ -38,6 +40,7 @@ export class AnnotatedUsersController extends BaseUsersController {
clearLoginAttempts, clearLoginAttempts,
increaseLoginAttempts, increaseLoginAttempts,
changeCredentialsUseCase, changeCredentialsUseCase,
getTransitionStatusUseCase,
) )
} }
@@ -51,6 +54,11 @@ export class AnnotatedUsersController extends BaseUsersController {
return super.keyParams(request) 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) @httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> { override async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
return super.deleteAccount(request, response) return super.deleteAccount(request, response)
@@ -285,6 +285,10 @@ export class BaseAuthController extends BaseHttpController {
authorizationHeader: <string>request.headers.authorization, 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) return this.json(result.data, result.status)
} }
@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
const user = authenticateRequestResponse.user as User 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, user,
session: authenticateRequestResponse.session, 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> { async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
} }
const { userUuid, settingName } = request.params const { userUuid, settingName } = request.params
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() }) const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (resultOrError.isFailed()) {
if (result.success) { return this.json(
return this.json(result) {
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> { 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> { 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, userUuid: response.locals.user.uuid,
settingName: request.params.subscriptionSettingName.toUpperCase(), settingName: request.params.subscriptionSettingName.toUpperCase(),
}) })
if (result.success) { if (resultOrError.isFailed()) {
return this.json(result) 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 { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser' import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses' import { ErrorTag } from '@standardnotes/responses'
import { GetTransitionStatus } from '../../../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
export class BaseUsersController extends BaseHttpController { export class BaseUsersController extends BaseHttpController {
constructor( constructor(
@@ -20,6 +21,7 @@ export class BaseUsersController extends BaseHttpController {
protected clearLoginAttempts: ClearLoginAttempts, protected clearLoginAttempts: ClearLoginAttempts,
protected increaseLoginAttempts: IncreaseLoginAttempts, protected increaseLoginAttempts: IncreaseLoginAttempts,
protected changeCredentialsUseCase: ChangeCredentials, protected changeCredentialsUseCase: ChangeCredentials,
protected getTransitionStatusUseCase: GetTransitionStatus,
private controllerContainer?: ControllerContainerInterface, private controllerContainer?: ControllerContainerInterface,
) { ) {
super() 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.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.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.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) 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> { async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) { if (request.params.userUuid !== response.locals.user.uuid) {
return this.json( 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, 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
}
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.50.1](https://github.com/standardnotes/server/compare/@standardnotes/common@1.50.0...@standardnotes/common@1.50.1) (2023-08-11)
**Note:** Version bump only for package @standardnotes/common
# [1.50.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.49.0...@standardnotes/common@1.50.0) (2023-07-12) # [1.50.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.49.0...@standardnotes/common@1.50.0) (2023-07-12)
### Features ### Features

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