Compare commits

..

81 Commits

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

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

* fix: use case binding

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

* fix: add role names emptyness validation

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

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

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

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

---------

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

* fix: rename inversify express controllers to annotated controllers
2023-08-01 09:34:52 +02:00
268 changed files with 3870 additions and 1961 deletions

8
.github/ci.env vendored
View File

@@ -10,7 +10,7 @@ REDIS_HOST=cache
AUTH_SERVER_ACCESS_TOKEN_AGE=4
AUTH_SERVER_REFRESH_TOKEN_AGE=10
AUTH_SERVER_EPHEMERAL_SESSION_AGE=300
SYNCING_SERVER_REVISIONS_FREQUENCY=5
SYNCING_SERVER_REVISIONS_FREQUENCY=2
AUTH_SERVER_LOG_LEVEL=debug
SYNCING_SERVER_LOG_LEVEL=debug
FILES_SERVER_LOG_LEVEL=debug
@@ -22,6 +22,12 @@ MYSQL_USER=std_notes_user
MYSQL_PASSWORD=changeme123
MYSQL_ROOT_PASSWORD=changeme123
MONGO_HOST=secondary_db
MONGO_PORT=27017
MONGO_USERNAME=standardnotes
MONGO_PASSWORD=standardnotes
MONGO_DATABASE=standardnotes
AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f

View File

@@ -20,6 +20,10 @@ on:
jobs:
e2e:
name: (Docker) E2E Test Suite
strategy:
fail-fast: false
matrix:
secondary_db_enabled: [true, false]
runs-on: ubuntu-latest
services:
@@ -45,12 +49,13 @@ jobs:
env:
DB_TYPE: mysql
CACHE_TYPE: redis
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
- name: Wait for server to start
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
- name: Show logs on failure
if: ${{ failure() }}
@@ -67,13 +72,7 @@ jobs:
matrix:
db_type: [mysql, sqlite]
cache_type: [redis, memory]
include:
- cache_type: redis
db_type: mysql
redis_port: 6380
- cache_type: redis
db_type: sqlite
redis_port: 6381
secondary_db_enabled: [true, false]
runs-on: ubuntu-latest
@@ -85,16 +84,24 @@ jobs:
cache:
image: redis
ports:
- ${{ matrix.redis_port }}:6379
- 6379:6379
db:
image: mysql
ports:
- 3307:3306
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: standardnotes_${{ matrix.cache_type }}
MYSQL_DATABASE: standardnotes
MYSQL_USER: standardnotes
MYSQL_PASSWORD: standardnotes
secondary_db:
image: mongo:5.0
ports:
- 27017:27017
env:
MONGO_INITDB_ROOT_USERNAME: standardnotes
MONGO_INITDB_ROOT_PASSWORD: standardnotes
MONGO_INITDB_DATABASE: standardnotes
steps:
- uses: actions/checkout@v3
@@ -123,16 +130,22 @@ jobs:
sed -i "s/VALET_TOKEN_SECRET=/VALET_TOKEN_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
echo "DB_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3307" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes_${{ matrix.cache_type }}" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=sqlite_${{ matrix.cache_type }}.db" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=homeserver.db" >> packages/home-server/.env
echo "DB_USERNAME=standardnotes" >> packages/home-server/.env
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:${{ matrix.redis_port }}" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env
echo "MONGO_HOST=localhost" >> packages/home-server/.env
echo "MONGO_PORT=27017" >> packages/home-server/.env
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
echo "MONGO_USERNAME=standardnotes" >> packages/home-server/.env
echo "MONGO_PASSWORD=standardnotes" >> packages/home-server/.env
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
echo "E2E_TESTING=true" >> packages/home-server/.env
@@ -145,7 +158,7 @@ jobs:
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
- name: Show logs on failure
if: ${{ failure() }}

231
.pnp.cjs generated
View File

@@ -5191,6 +5191,7 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.5.0"],\
["jsonwebtoken", "npm:9.0.0"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mysql2", "npm:3.3.3"],\
["newrelic", "npm:10.1.2"],\
["nodemon", "npm:2.0.22"],\
@@ -5201,7 +5202,7 @@ const RAW_RUNTIME_STATE =
["semver", "npm:7.5.1"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.16"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\
["uuid", "npm:9.0.0"],\
@@ -5259,7 +5260,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/responses", "npm:1.13.27"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.17.5"],\
["@types/cors", "npm:2.8.13"],\
["@types/express", "npm:4.17.17"],\
["@types/ioredis", "npm:5.0.0"],\
@@ -5870,6 +5870,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/webidl-conversions", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@types-webidl-conversions-npm-7.0.0-0903313151-86c337dc1e.zip/node_modules/@types/webidl-conversions/",\
"packageDependencies": [\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/whatwg-url", [\
["npm:8.2.2", {\
"packageLocation": "./.yarn/cache/@types-whatwg-url-npm-8.2.2-54c5c24e6c-25f20f5649.zip/node_modules/@types/whatwg-url/",\
"packageDependencies": [\
["@types/whatwg-url", "npm:8.2.2"],\
["@types/node", "npm:20.2.5"],\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/yargs", [\
["npm:17.0.24", {\
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\
@@ -7075,6 +7095,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bson", [\
["npm:5.4.0", {\
"packageLocation": "./.yarn/cache/bson-npm-5.4.0-2f854c8216-2c913a45c0.zip/node_modules/bson/",\
"packageDependencies": [\
["bson", "npm:5.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["buffer", [\
["npm:5.7.1", {\
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\
@@ -11933,6 +11962,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["memory-pager", [\
["npm:1.5.0", {\
"packageLocation": "./.yarn/cache/memory-pager-npm-1.5.0-46e20e6c81-6b00ff499b.zip/node_modules/memory-pager/",\
"packageDependencies": [\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["meow", [\
["npm:8.1.2", {\
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\
@@ -12291,6 +12329,59 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mongodb", [\
["npm:5.7.0", {\
"packageLocation": "./.yarn/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "npm:5.7.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0", {\
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-eb0cd47e23/0/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["@aws-sdk/credential-providers", null],\
["@mongodb-js/zstd", null],\
["@types/aws-sdk__credential-providers", null],\
["@types/kerberos", null],\
["@types/mongodb-client-encryption", null],\
["@types/mongodb-js__zstd", null],\
["@types/snappy", null],\
["bson", "npm:5.4.0"],\
["kerberos", null],\
["mongodb-client-encryption", null],\
["mongodb-connection-string-url", "npm:2.6.0"],\
["saslprep", "npm:1.0.3"],\
["snappy", null],\
["socks", "npm:2.7.1"]\
],\
"packagePeers": [\
"@aws-sdk/credential-providers",\
"@mongodb-js/zstd",\
"@types/aws-sdk__credential-providers",\
"@types/kerberos",\
"@types/mongodb-client-encryption",\
"@types/mongodb-js__zstd",\
"@types/snappy",\
"kerberos",\
"mongodb-client-encryption",\
"snappy"\
],\
"linkType": "HARD"\
}]\
]],\
["mongodb-connection-string-url", [\
["npm:2.6.0", {\
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-8a9186dd1b.zip/node_modules/mongodb-connection-string-url/",\
"packageDependencies": [\
["mongodb-connection-string-url", "npm:2.6.0"],\
["@types/whatwg-url", "npm:8.2.2"],\
["whatwg-url", "npm:11.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ms", [\
["npm:2.0.0", {\
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\
@@ -14250,6 +14341,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["saslprep", [\
["npm:1.0.3", {\
"packageLocation": "./.yarn/cache/saslprep-npm-1.0.3-8db649c346-23ebcda091.zip/node_modules/saslprep/",\
"packageDependencies": [\
["saslprep", "npm:1.0.3"],\
["sparse-bitfield", "npm:3.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["schema-utils", [\
["npm:3.1.2", {\
"packageLocation": "./.yarn/cache/schema-utils-npm-3.1.2-d97c6dc247-11d35f997e.zip/node_modules/schema-utils/",\
@@ -14605,6 +14706,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["sparse-bitfield", [\
["npm:3.0.3", {\
"packageLocation": "./.yarn/cache/sparse-bitfield-npm-3.0.3-cb80d0c89f-625ecdf6f4.zip/node_modules/sparse-bitfield/",\
"packageDependencies": [\
["sparse-bitfield", "npm:3.0.3"],\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["spawn-please", [\
["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\
@@ -15247,6 +15358,14 @@ const RAW_RUNTIME_STATE =
["tr46", "npm:0.0.3"]\
],\
"linkType": "HARD"\
}],\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/tr46-npm-3.0.0-e1ae1ea7c9-3a481676bf.zip/node_modules/tr46/",\
"packageDependencies": [\
["tr46", "npm:3.0.0"],\
["punycode", "npm:2.3.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["treeverse", [\
@@ -15758,6 +15877,98 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-13b6364fde/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", null],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\
["@types/ioredis", null],\
["@types/mongodb", null],\
["@types/mssql", null],\
["@types/mysql2", null],\
["@types/oracledb", null],\
["@types/pg", null],\
["@types/pg-native", null],\
["@types/pg-query-stream", null],\
["@types/redis", null],\
["@types/sap__hana-client", null],\
["@types/sql.js", null],\
["@types/sqlite3", null],\
["@types/ts-node", null],\
["@types/typeorm-aurora-data-api-driver", null],\
["app-root-path", "npm:3.1.0"],\
["better-sqlite3", null],\
["buffer", "npm:6.0.3"],\
["chalk", "npm:4.1.2"],\
["cli-highlight", "npm:2.1.11"],\
["date-fns", "npm:2.30.0"],\
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"],\
["dotenv", "npm:16.1.3"],\
["glob", "npm:8.1.0"],\
["hdb-pool", null],\
["ioredis", null],\
["mkdirp", "npm:2.1.6"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mssql", null],\
["mysql2", "npm:3.3.3"],\
["oracledb", null],\
["pg", null],\
["pg-native", null],\
["pg-query-stream", null],\
["redis", null],\
["reflect-metadata", "npm:0.1.13"],\
["sha.js", "npm:2.4.11"],\
["sql.js", null],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-node", null],\
["tslib", "npm:2.5.2"],\
["typeorm-aurora-data-api-driver", null],\
["uuid", "npm:9.0.0"],\
["yargs", "npm:17.7.2"]\
],\
"packagePeers": [\
"@google-cloud/spanner",\
"@sap/hana-client",\
"@types/better-sqlite3",\
"@types/google-cloud__spanner",\
"@types/hdb-pool",\
"@types/ioredis",\
"@types/mongodb",\
"@types/mssql",\
"@types/mysql2",\
"@types/oracledb",\
"@types/pg-native",\
"@types/pg-query-stream",\
"@types/pg",\
"@types/redis",\
"@types/sap__hana-client",\
"@types/sql.js",\
"@types/sqlite3",\
"@types/ts-node",\
"@types/typeorm-aurora-data-api-driver",\
"better-sqlite3",\
"hdb-pool",\
"ioredis",\
"mongodb",\
"mssql",\
"mysql2",\
"oracledb",\
"pg-native",\
"pg-query-stream",\
"pg",\
"redis",\
"sql.js",\
"sqlite3",\
"ts-node",\
"typeorm-aurora-data-api-driver"\
],\
"linkType": "HARD"\
}],\
["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-fc9b7b780b/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
@@ -16192,6 +16403,13 @@ const RAW_RUNTIME_STATE =
["webidl-conversions", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-bdbe11c68c.zip/node_modules/webidl-conversions/",\
"packageDependencies": [\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["webpack", [\
@@ -16250,6 +16468,15 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["whatwg-url", [\
["npm:11.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-11.0.0-073529d93a-ee3a532bfb.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
["whatwg-url", "npm:11.0.0"],\
["tr46", "npm:3.0.0"],\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\
"packageDependencies": [\

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,6 +23,7 @@ services:
environment:
DB_TYPE: "${DB_TYPE}"
CACHE_TYPE: "${CACHE_TYPE}"
SECONDARY_DB_ENABLED: "${SECONDARY_DB_ENABLED}"
container_name: server-ci
ports:
- 3123:3000
@@ -61,6 +62,21 @@ services:
networks:
- standardnotes_self_hosted
secondary_db:
image: mongo:5.0
container_name: secondary_db-ci
expose:
- 27017
restart: unless-stopped
volumes:
- ./data/mongo:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: standardnotes
MONGO_INITDB_ROOT_PASSWORD: standardnotes
MONGO_INITDB_DATABASE: standardnotes
networks:
- standardnotes_self_hosted
cache:
image: redis:6.0-alpine
container_name: cache-ci

View File

@@ -63,6 +63,9 @@ fi
if [ -z "$CACHE_TYPE" ]; then
export CACHE_TYPE="redis"
fi
if [ -z "$SECONDARY_DB_ENABLED" ]; then
export SECONDARY_DB_ENABLED=false
fi
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
#########

View File

@@ -12,13 +12,15 @@
},
"scripts": {
"lint": "yarn workspaces foreach -p -j 10 --verbose run lint",
"lint:fix": "yarn workspaces foreach -p -j 10 --verbose run lint:fix",
"clean": "yarn workspaces foreach -p --verbose run clean",
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
"postversion": "./scripts/push-tags-one-by-one.sh",
"upgrade:snjs": "yarn workspaces foreach --verbose run upgrade:snjs",
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start"
"e2e": "yarn build packages/home-server && PORT=3123 yarn workspace @standardnotes/home-server start",
"start": "yarn build packages/home-server && yarn workspace @standardnotes/home-server start"
},
"devDependencies": {
"@commitlint/cli": "^17.0.2",

View File

@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.11...@standardnotes/analytics@2.25.12) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.10...@standardnotes/analytics@2.25.11) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.9...@standardnotes/analytics@2.25.10) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.8...@standardnotes/analytics@2.25.9) (2023-08-08)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.7...@standardnotes/analytics@2.25.8) (2023-08-03)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.6...@standardnotes/analytics@2.25.7) (2023-08-02)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.5...@standardnotes/analytics@2.25.6) (2023-07-27)
**Note:** Version bump only for package @standardnotes/analytics

View File

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

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.2...@standardnotes/api-gateway@1.70.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.1...@standardnotes/api-gateway@1.70.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.0...@standardnotes/api-gateway@1.70.1) (2023-08-08)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.70.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.3...@standardnotes/api-gateway@1.70.0) (2023-08-07)
### Features
* **syncing-server:** limit shared vaults creation based on role ([#687](https://github.com/standardnotes/api-gateway/issues/687)) ([19b8921](https://github.com/standardnotes/api-gateway/commit/19b8921f286ff8f88c427e8ddd4512a8d61edb4f))
## [1.69.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.2...@standardnotes/api-gateway@1.69.3) (2023-08-03)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.69.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.1...@standardnotes/api-gateway@1.69.2) (2023-08-02)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.69.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.69.0...@standardnotes/api-gateway@1.69.1) (2023-07-31)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.69.1",
"version": "1.70.4",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -1,5 +1,4 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { TimerInterface } from '@standardnotes/time'
import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils'
@@ -51,10 +50,6 @@ export abstract class AuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,

View File

@@ -1,5 +1,4 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import { RoleName } from '@standardnotes/domain-core'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils'
@@ -60,9 +59,6 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.freeUser =
decodedToken.roles.length === 1 &&
decodedToken.roles.find((role) => role.name === RoleName.NAMES.CoreUser) !== undefined
response.locals.user = decodedToken.user
response.locals.roles = decodedToken.roles
} catch (error) {

View File

@@ -3,6 +3,68 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
### Bug Fixes
* **auth:** passing the invalidate cache header ([#697](https://github.com/standardnotes/server/issues/697)) ([83ad069](https://github.com/standardnotes/server/commit/83ad069c5dd9afa3a6db881f0d8a55a58d0642aa))
## [1.131.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.3...@standardnotes/auth-server@1.131.4) (2023-08-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.2...@standardnotes/auth-server@1.131.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.1...@standardnotes/auth-server@1.131.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.0...@standardnotes/auth-server@1.131.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.131.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.1...@standardnotes/auth-server@1.131.0) (2023-08-08)
### Features
* update storage quota used for user based on shared vault files ([#689](https://github.com/standardnotes/server/issues/689)) ([5311e74](https://github.com/standardnotes/server/commit/5311e7426617da6fc75593dd0fcbff589ca4fc22))
## [1.130.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.0...@standardnotes/auth-server@1.130.1) (2023-08-07)
### Bug Fixes
* **auth:** update user agent upon refreshing session token ([#685](https://github.com/standardnotes/server/issues/685)) ([bd5f492](https://github.com/standardnotes/server/commit/bd5f492a733f783c64fa4bc5840b4a9f5c913d3d))
# [1.130.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.129.0...@standardnotes/auth-server@1.130.0) (2023-08-07)
### Features
* **auth:** invalidate other sessions for user if the email or password are changed ([#684](https://github.com/standardnotes/server/issues/684)) ([f39d3ac](https://github.com/standardnotes/server/commit/f39d3aca5b7bb9e5f9c1c24cbe2359f30dea835c))
# [1.129.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.128.1...@standardnotes/auth-server@1.129.0) (2023-08-03)
### Features
* **auth:** add handling payments account deleted events STA-1769 ([#682](https://github.com/standardnotes/server/issues/682)) ([8e35dfa](https://github.com/standardnotes/server/commit/8e35dfa4b77256f4c0a3294b296a5526fd1020ad))
## [1.128.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.128.0...@standardnotes/auth-server@1.128.1) (2023-08-02)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.128.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.127.2...@standardnotes/auth-server@1.128.0) (2023-08-02)
### Features
* enable Write Ahead Log mode for SQLite ([#681](https://github.com/standardnotes/server/issues/681)) ([8cd7a13](https://github.com/standardnotes/server/commit/8cd7a138ab56f6a2b0d6c06ef6041ab9b85ae540))
## [1.127.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.127.1...@standardnotes/auth-server@1.127.2) (2023-08-01)
### Bug Fixes
* controller naming ([#678](https://github.com/standardnotes/server/issues/678)) ([56f0aef](https://github.com/standardnotes/server/commit/56f0aef21d3fcec7ac7e968cb1c1b071becbbe26))
## [1.127.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.127.0...@standardnotes/auth-server@1.127.1) (2023-07-31)
### Bug Fixes

View File

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

View File

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

View File

@@ -38,7 +38,7 @@ import { GetUserKeyParams } from '../Domain/UseCase/GetUserKeyParams/GetUserKeyP
import { UpdateUser } from '../Domain/UseCase/UpdateUser'
import { RedisEphemeralSessionRepository } from '../Infra/Redis/RedisEphemeralSessionRepository'
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
import { DeletePreviousSessionsForUser } from '../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../Domain/UseCase/DeleteSessionForUser'
import { Register } from '../Domain/UseCase/Register'
import { LockRepository } from '../Infra/Redis/LockRepository'
@@ -234,24 +234,28 @@ import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middle
import { LockMiddleware } from '../Infra/InversifyExpressUtils/Middleware/LockMiddleware'
import { RequiredCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OptionalCrossServiceTokenMiddleware'
import { HomeServerSettingsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSettingsController'
import { HomeServerAdminController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAdminController'
import { HomeServerAuthController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAuthController'
import { HomeServerAuthenticatorsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAuthenticatorsController'
import { HomeServerFeaturesController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerFeaturesController'
import { HomeServerListedController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerListedController'
import { HomeServerOfflineController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerOfflineController'
import { HomeServerSessionController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionController'
import { HomeServerSubscriptionInvitesController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionInvitesController'
import { HomeServerSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionSettingsController'
import { HomeServerSubscriptionTokensController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionTokensController'
import { HomeServerUserRequestsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerUserRequestsController'
import { HomeServerUsersController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerUsersController'
import { HomeServerValetTokenController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerValetTokenController'
import { HomeServerWebSocketsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController'
import { HomeServerSessionsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController'
import { BaseSettingsController } from '../Infra/InversifyExpressUtils/Base/BaseSettingsController'
import { BaseAdminController } from '../Infra/InversifyExpressUtils/Base/BaseAdminController'
import { BaseAuthController } from '../Infra/InversifyExpressUtils/Base/BaseAuthController'
import { BaseAuthenticatorsController } from '../Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController'
import { BaseFeaturesController } from '../Infra/InversifyExpressUtils/Base/BaseFeaturesController'
import { BaseListedController } from '../Infra/InversifyExpressUtils/Base/BaseListedController'
import { BaseOfflineController } from '../Infra/InversifyExpressUtils/Base/BaseOfflineController'
import { BaseSessionController } from '../Infra/InversifyExpressUtils/Base/BaseSessionController'
import { BaseSubscriptionInvitesController } from '../Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController'
import { BaseSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController'
import { BaseSubscriptionTokensController } from '../Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController'
import { BaseUserRequestsController } from '../Infra/InversifyExpressUtils/Base/BaseUserRequestsController'
import { BaseUsersController } from '../Infra/InversifyExpressUtils/Base/BaseUsersController'
import { BaseValetTokenController } from '../Infra/InversifyExpressUtils/Base/BaseValetTokenController'
import { BaseWebSocketsController } from '../Infra/InversifyExpressUtils/Base/BaseWebSocketsController'
import { BaseSessionsController } from '../Infra/InversifyExpressUtils/Base/BaseSessionsController'
import { Transform } from 'stream'
import { ActivatePremiumFeatures } from '../Domain/UseCase/ActivatePremiumFeatures/ActivatePremiumFeatures'
import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAccountDeletedEventHandler'
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -826,9 +830,7 @@ export class ContainerConfigLoader {
container.bind<UpdateUser>(TYPES.Auth_UpdateUser).to(UpdateUser)
container.bind<Register>(TYPES.Auth_Register).to(Register)
container.bind<GetActiveSessionsForUser>(TYPES.Auth_GetActiveSessionsForUser).to(GetActiveSessionsForUser)
container
.bind<DeletePreviousSessionsForUser>(TYPES.Auth_DeletePreviousSessionsForUser)
.to(DeletePreviousSessionsForUser)
container.bind<DeleteOtherSessionsForUser>(TYPES.Auth_DeleteOtherSessionsForUser).to(DeleteOtherSessionsForUser)
container.bind<DeleteSessionForUser>(TYPES.Auth_DeleteSessionForUser).to(DeleteSessionForUser)
container.bind<ChangeCredentials>(TYPES.Auth_ChangeCredentials).to(ChangeCredentials)
container.bind<GetSettings>(TYPES.Auth_GetSettings).to(GetSettings)
@@ -883,6 +885,15 @@ export class ContainerConfigLoader {
container.bind<VerifyPredicate>(TYPES.Auth_VerifyPredicate).to(VerifyPredicate)
container.bind<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken).to(CreateCrossServiceToken)
container.bind<ProcessUserRequest>(TYPES.Auth_ProcessUserRequest).to(ProcessUserRequest)
container
.bind<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser)
.toConstantValue(
new UpdateStorageQuotaUsedForUser(
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_UserSubscriptionService),
container.get(TYPES.Auth_SubscriptionSettingService),
),
)
// Controller
container
@@ -952,8 +963,38 @@ export class ContainerConfigLoader {
container
.bind<UserEmailChangedEventHandler>(TYPES.Auth_UserEmailChangedEventHandler)
.to(UserEmailChangedEventHandler)
container.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler).to(FileUploadedEventHandler)
container.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler).to(FileRemovedEventHandler)
container
.bind<FileUploadedEventHandler>(TYPES.Auth_FileUploadedEventHandler)
.toConstantValue(
new FileUploadedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileUploadedEventHandler>(TYPES.Auth_SharedVaultFileUploadedEventHandler)
.toConstantValue(
new SharedVaultFileUploadedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
.toConstantValue(
new FileRemovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileRemovedEventHandler>(TYPES.Auth_SharedVaultFileRemovedEventHandler)
.toConstantValue(
new SharedVaultFileRemovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<ListedAccountCreatedEventHandler>(TYPES.Auth_ListedAccountCreatedEventHandler)
.to(ListedAccountCreatedEventHandler)
@@ -978,6 +1019,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_SettingService),
),
)
container
.bind<PaymentsAccountDeletedEventHandler>(TYPES.Auth_PaymentsAccountDeletedEventHandler)
.toConstantValue(
new PaymentsAccountDeletedEventHandler(
container.get(TYPES.Auth_DeleteAccount),
container.get(TYPES.Auth_Logger),
),
)
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
@@ -992,7 +1041,9 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_REASSIGNED', container.get(TYPES.Auth_SubscriptionReassignedEventHandler)],
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],
['LISTED_ACCOUNT_DELETED', container.get(TYPES.Auth_ListedAccountDeletedEventHandler)],
[
@@ -1005,6 +1056,7 @@ export class ContainerConfigLoader {
],
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.Auth_PredicateVerificationRequestedEventHandler)],
['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
])
if (isConfiguredForHomeServer) {
@@ -1037,9 +1089,9 @@ export class ContainerConfigLoader {
}
container
.bind<HomeServerAuthController>(TYPES.Auth_HomeServerAuthController)
.bind<BaseAuthController>(TYPES.Auth_BaseAuthController)
.toConstantValue(
new HomeServerAuthController(
new BaseAuthController(
container.get(TYPES.Auth_VerifyMFA),
container.get(TYPES.Auth_SignIn),
container.get(TYPES.Auth_GetUserKeyParams),
@@ -1054,42 +1106,42 @@ export class ContainerConfigLoader {
// Inversify Controllers
if (isConfiguredForHomeServer) {
container
.bind<HomeServerAuthenticatorsController>(TYPES.Auth_HomeServerAuthenticatorsController)
.bind<BaseAuthenticatorsController>(TYPES.Auth_BaseAuthenticatorsController)
.toConstantValue(
new HomeServerAuthenticatorsController(
new BaseAuthenticatorsController(
container.get(TYPES.Auth_AuthenticatorsController),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerSubscriptionInvitesController>(TYPES.Auth_HomeServerSubscriptionInvitesController)
.bind<BaseSubscriptionInvitesController>(TYPES.Auth_BaseSubscriptionInvitesController)
.toConstantValue(
new HomeServerSubscriptionInvitesController(
new BaseSubscriptionInvitesController(
container.get(TYPES.Auth_SubscriptionInvitesController),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerUserRequestsController>(TYPES.Auth_HomeServerUserRequestsController)
.bind<BaseUserRequestsController>(TYPES.Auth_BaseUserRequestsController)
.toConstantValue(
new HomeServerUserRequestsController(
new BaseUserRequestsController(
container.get(TYPES.Auth_UserRequestsController),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerWebSocketsController>(TYPES.Auth_HomeServerWebSocketsController)
.bind<BaseWebSocketsController>(TYPES.Auth_BaseWebSocketsController)
.toConstantValue(
new HomeServerWebSocketsController(
new BaseWebSocketsController(
container.get(TYPES.Auth_CreateCrossServiceToken),
container.get(TYPES.Auth_WebSocketConnectionTokenDecoder),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerSessionsController>(TYPES.Auth_HomeServerSessionsController)
.bind<BaseSessionsController>(TYPES.Auth_BaseSessionsController)
.toConstantValue(
new HomeServerSessionsController(
new BaseSessionsController(
container.get(TYPES.Auth_GetActiveSessionsForUser),
container.get(TYPES.Auth_AuthenticateRequest),
container.get(TYPES.Auth_SessionProjector),
@@ -1098,17 +1150,17 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerValetTokenController>(TYPES.Auth_HomeServerValetTokenController)
.bind<BaseValetTokenController>(TYPES.Auth_BaseValetTokenController)
.toConstantValue(
new HomeServerValetTokenController(
new BaseValetTokenController(
container.get(TYPES.Auth_CreateValetToken),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerUsersController>(TYPES.Auth_HomeServerUsersController)
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue(
new HomeServerUsersController(
new BaseUsersController(
container.get(TYPES.Auth_UpdateUser),
container.get(TYPES.Auth_GetUserKeyParams),
container.get(TYPES.Auth_DeleteAccount),
@@ -1120,9 +1172,9 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerAdminController>(TYPES.Auth_HomeServerAdminController)
.bind<BaseAdminController>(TYPES.Auth_BaseAdminController)
.toConstantValue(
new HomeServerAdminController(
new BaseAdminController(
container.get(TYPES.Auth_DeleteSetting),
container.get(TYPES.Auth_UserRepository),
container.get(TYPES.Auth_CreateSubscriptionToken),
@@ -1131,9 +1183,9 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerSubscriptionTokensController>(TYPES.Auth_HomeServerSubscriptionTokensController)
.bind<BaseSubscriptionTokensController>(TYPES.Auth_BaseSubscriptionTokensController)
.toConstantValue(
new HomeServerSubscriptionTokensController(
new BaseSubscriptionTokensController(
container.get(TYPES.Auth_CreateSubscriptionToken),
container.get(TYPES.Auth_AuthenticateSubscriptionToken),
container.get(TYPES.Auth_SettingService),
@@ -1145,17 +1197,17 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerSubscriptionSettingsController>(TYPES.Auth_HomeServerSubscriptionSettingsController)
.bind<BaseSubscriptionSettingsController>(TYPES.Auth_BaseSubscriptionSettingsController)
.toConstantValue(
new HomeServerSubscriptionSettingsController(
new BaseSubscriptionSettingsController(
container.get(TYPES.Auth_GetSetting),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerSettingsController>(TYPES.Auth_HomeServerSettingsController)
.bind<BaseSettingsController>(TYPES.Auth_BaseSettingsController)
.toConstantValue(
new HomeServerSettingsController(
new BaseSettingsController(
container.get(TYPES.Auth_GetSettings),
container.get(TYPES.Auth_GetSetting),
container.get(TYPES.Auth_UpdateSetting),
@@ -1164,19 +1216,19 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerSessionController>(TYPES.Auth_HomeServerSessionController)
.bind<BaseSessionController>(TYPES.Auth_BaseSessionController)
.toConstantValue(
new HomeServerSessionController(
new BaseSessionController(
container.get(TYPES.Auth_DeleteSessionForUser),
container.get(TYPES.Auth_DeletePreviousSessionsForUser),
container.get(TYPES.Auth_DeleteOtherSessionsForUser),
container.get(TYPES.Auth_RefreshSessionToken),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerOfflineController>(TYPES.Auth_HomeServerOfflineController)
.bind<BaseOfflineController>(TYPES.Auth_BaseOfflineController)
.toConstantValue(
new HomeServerOfflineController(
new BaseOfflineController(
container.get(TYPES.Auth_GetUserFeatures),
container.get(TYPES.Auth_GetUserOfflineSubscription),
container.get(TYPES.Auth_CreateOfflineSubscriptionToken),
@@ -1188,17 +1240,17 @@ export class ContainerConfigLoader {
),
)
container
.bind<HomeServerListedController>(TYPES.Auth_HomeServerListedController)
.bind<BaseListedController>(TYPES.Auth_BaseListedController)
.toConstantValue(
new HomeServerListedController(
new BaseListedController(
container.get(TYPES.Auth_CreateListedAccount),
container.get(TYPES.Auth_ControllerContainer),
),
)
container
.bind<HomeServerFeaturesController>(TYPES.Auth_HomeServerFeaturesController)
.bind<BaseFeaturesController>(TYPES.Auth_BaseFeaturesController)
.toConstantValue(
new HomeServerFeaturesController(
new BaseFeaturesController(
container.get(TYPES.Auth_GetUserFeatures),
container.get(TYPES.Auth_ControllerContainer),
),

View File

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

View File

@@ -113,7 +113,7 @@ const TYPES = {
Auth_UpdateUser: Symbol.for('Auth_UpdateUser'),
Auth_Register: Symbol.for('Auth_Register'),
Auth_GetActiveSessionsForUser: Symbol.for('Auth_GetActiveSessionsForUser'),
Auth_DeletePreviousSessionsForUser: Symbol.for('Auth_DeletePreviousSessionsForUser'),
Auth_DeleteOtherSessionsForUser: Symbol.for('Auth_DeleteOtherSessionsForUser'),
Auth_DeleteSessionForUser: Symbol.for('Auth_DeleteSessionForUser'),
Auth_ChangeCredentials: Symbol.for('Auth_ChangePassword'),
Auth_GetSettings: Symbol.for('Auth_GetSettings'),
@@ -152,6 +152,7 @@ const TYPES = {
Auth_ActivatePremiumFeatures: Symbol.for('Auth_ActivatePremiumFeatures'),
Auth_SignInWithRecoveryCodes: Symbol.for('Auth_SignInWithRecoveryCodes'),
Auth_GetUserKeyParamsRecovery: Symbol.for('Auth_GetUserKeyParamsRecovery'),
Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
// Handlers
Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -165,7 +166,9 @@ const TYPES = {
Auth_ExtensionKeyGrantedEventHandler: Symbol.for('Auth_ExtensionKeyGrantedEventHandler'),
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),
Auth_ListedAccountDeletedEventHandler: Symbol.for('Auth_ListedAccountDeletedEventHandler'),
Auth_UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for(
@@ -176,6 +179,7 @@ const TYPES = {
),
Auth_PredicateVerificationRequestedEventHandler: Symbol.for('Auth_PredicateVerificationRequestedEventHandler'),
Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
// Services
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
Auth_SessionService: Symbol.for('Auth_SessionService'),
@@ -217,22 +221,22 @@ const TYPES = {
Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
Auth_HomeServerAuthController: Symbol.for('Auth_HomeServerAuthController'),
Auth_HomeServerAuthenticatorsController: Symbol.for('Auth_HomeServerAuthenticatorsController'),
Auth_HomeServerSubscriptionInvitesController: Symbol.for('Auth_HomeServerSubscriptionInvitesController'),
Auth_HomeServerUserRequestsController: Symbol.for('Auth_HomeServerUserRequestsController'),
Auth_HomeServerWebSocketsController: Symbol.for('Auth_HomeServerWebSocketsController'),
Auth_HomeServerSessionsController: Symbol.for('Auth_HomeServerSessionsController'),
Auth_HomeServerValetTokenController: Symbol.for('Auth_HomeServerValetTokenController'),
Auth_HomeServerUsersController: Symbol.for('Auth_HomeServerUsersController'),
Auth_HomeServerAdminController: Symbol.for('Auth_HomeServerAdminController'),
Auth_HomeServerSubscriptionTokensController: Symbol.for('Auth_HomeServerSubscriptionTokensController'),
Auth_HomeServerSubscriptionSettingsController: Symbol.for('Auth_HomeServerSubscriptionSettingsController'),
Auth_HomeServerSettingsController: Symbol.for('Auth_HomeServerSettingsController'),
Auth_HomeServerSessionController: Symbol.for('Auth_HomeServerSessionController'),
Auth_HomeServerOfflineController: Symbol.for('Auth_HomeServerOfflineController'),
Auth_HomeServerListedController: Symbol.for('Auth_HomeServerListedController'),
Auth_HomeServerFeaturesController: Symbol.for('Auth_HomeServerFeaturesController'),
Auth_BaseAuthController: Symbol.for('Auth_BaseAuthController'),
Auth_BaseAuthenticatorsController: Symbol.for('Auth_BaseAuthenticatorsController'),
Auth_BaseSubscriptionInvitesController: Symbol.for('Auth_BaseSubscriptionInvitesController'),
Auth_BaseUserRequestsController: Symbol.for('Auth_BaseUserRequestsController'),
Auth_BaseWebSocketsController: Symbol.for('Auth_BaseWebSocketsController'),
Auth_BaseSessionsController: Symbol.for('Auth_BaseSessionsController'),
Auth_BaseValetTokenController: Symbol.for('Auth_BaseValetTokenController'),
Auth_BaseUsersController: Symbol.for('Auth_BaseUsersController'),
Auth_BaseAdminController: Symbol.for('Auth_BaseAdminController'),
Auth_BaseSubscriptionTokensController: Symbol.for('Auth_BaseSubscriptionTokensController'),
Auth_BaseSubscriptionSettingsController: Symbol.for('Auth_BaseSubscriptionSettingsController'),
Auth_BaseSettingsController: Symbol.for('Auth_BaseSettingsController'),
Auth_BaseSessionController: Symbol.for('Auth_BaseSessionController'),
Auth_BaseOfflineController: Symbol.for('Auth_BaseOfflineController'),
Auth_BaseListedController: Symbol.for('Auth_BaseListedController'),
Auth_BaseFeaturesController: Symbol.for('Auth_BaseFeaturesController'),
}
export default TYPES

View File

@@ -30,7 +30,7 @@ describe('AuthResponseFactory20161215', () => {
})
it('should create a 20161215 auth response', async () => {
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20161215',
userAgent: 'Google Chrome',
@@ -38,7 +38,7 @@ describe('AuthResponseFactory20161215', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
user: { foo: 'bar' },
token: 'foobar',
})

View File

@@ -11,6 +11,7 @@ import { User } from '../User/User'
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
import { AuthResponseFactoryInterface } from './AuthResponseFactoryInterface'
import { Session } from '../Session/Session'
@injectable()
export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface {
@@ -26,7 +27,7 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<AuthResponse20161215 | AuthResponse20200115> {
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
this.logger.debug(`Creating JWT auth response for user ${dto.user.uuid}`)
const data: SessionTokenData = {
@@ -39,12 +40,14 @@ export class AuthResponseFactory20161215 implements AuthResponseFactoryInterface
this.logger.debug(`Created JWT token for user ${dto.user.uuid}: ${token}`)
return {
user: this.userProjector.projectSimple(dto.user) as {
uuid: string
email: string
protocolVersion: ProtocolVersion
response: {
user: this.userProjector.projectSimple(dto.user) as {
uuid: string
email: string
protocolVersion: ProtocolVersion
},
token,
},
token,
}
}
}

View File

@@ -29,7 +29,7 @@ describe('AuthResponseFactory20190520', () => {
})
it('should create a 20161215 auth response', async () => {
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20161215',
userAgent: 'Google Chrome',
@@ -37,7 +37,7 @@ describe('AuthResponseFactory20190520', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
user: { foo: 'bar' },
token: 'foobar',
})

View File

@@ -11,6 +11,7 @@ import { User } from '../User/User'
import { AuthResponseFactory20200115 } from './AuthResponseFactory20200115'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { Session } from '../Session/Session'
describe('AuthResponseFactory20200115', () => {
let sessionService: SessionServiceInterface
@@ -48,8 +49,12 @@ describe('AuthResponseFactory20200115', () => {
}
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.createNewSessionForUser = jest.fn().mockReturnValue(sessionPayload)
sessionService.createNewEphemeralSessionForUser = jest.fn().mockReturnValue(sessionPayload)
sessionService.createNewSessionForUser = jest
.fn()
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
sessionService.createNewEphemeralSessionForUser = jest
.fn()
.mockReturnValue({ sessionHttpRepresentation: sessionPayload, session: {} as jest.Mocked<Session> })
keyParamsFactory = {} as jest.Mocked<KeyParamsFactoryInterface>
keyParamsFactory.create = jest.fn().mockReturnValue({
@@ -76,7 +81,7 @@ describe('AuthResponseFactory20200115', () => {
it('should create a 20161215 auth response if user does not support sessions', async () => {
user.supportsSessions = jest.fn().mockReturnValue(false)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20161215',
userAgent: 'Google Chrome',
@@ -84,7 +89,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
user: { foo: 'bar' },
token: expect.any(String),
})
@@ -93,7 +98,7 @@ describe('AuthResponseFactory20200115', () => {
it('should create a 20200115 auth response', async () => {
user.supportsSessions = jest.fn().mockReturnValue(true)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -101,7 +106,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',
@@ -124,7 +129,7 @@ describe('AuthResponseFactory20200115', () => {
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))
user.supportsSessions = jest.fn().mockReturnValue(true)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -132,7 +137,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',
@@ -153,7 +158,7 @@ describe('AuthResponseFactory20200115', () => {
it('should create a 20200115 auth response with an ephemeral session', async () => {
user.supportsSessions = jest.fn().mockReturnValue(true)
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -161,7 +166,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: false,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',
@@ -183,11 +188,14 @@ describe('AuthResponseFactory20200115', () => {
user.supportsSessions = jest.fn().mockReturnValue(true)
sessionService.createNewSessionForUser = jest.fn().mockReturnValue({
...sessionPayload,
readonly_access: true,
sessionHttpRepresentation: {
...sessionPayload,
readonly_access: true,
},
session: {} as jest.Mocked<Session>,
})
const response = await createFactory().createResponse({
const result = await createFactory().createResponse({
user,
apiVersion: '20200115',
userAgent: 'Google Chrome',
@@ -195,7 +203,7 @@ describe('AuthResponseFactory20200115', () => {
readonlyAccess: true,
})
expect(response).toEqual({
expect(result.response).toEqual({
key_params: {
key1: 'value1',
key2: 'value2',

View File

@@ -19,6 +19,7 @@ import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterfac
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
import { Session } from '../Session/Session'
@injectable()
export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
@@ -40,21 +41,28 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<AuthResponse20161215 | AuthResponse20200115> {
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }> {
if (!dto.user.supportsSessions()) {
this.logger.debug(`User ${dto.user.uuid} does not support sessions. Falling back to JWT auth response`)
return super.createResponse(dto)
}
const sessionPayload = await this.createSession(dto)
const sessionCreationResult = await this.createSession(dto)
this.logger.debug('Created session payload for user %s: %O', dto.user.uuid, sessionPayload)
this.logger.debug(
'Created session payload for user %s: %O',
dto.user.uuid,
sessionCreationResult.sessionHttpRepresentation,
)
return {
session: sessionPayload,
key_params: this.keyParamsFactory.create(dto.user, true),
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
response: {
session: sessionCreationResult.sessionHttpRepresentation,
key_params: this.keyParamsFactory.create(dto.user, true),
user: this.userProjector.projectSimple(dto.user) as SimpleUserProjection,
},
session: sessionCreationResult.session,
}
}
@@ -64,12 +72,12 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<SessionBody> {
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
if (dto.ephemeralSession) {
return this.sessionService.createNewEphemeralSessionForUser(dto)
}
const session = this.sessionService.createNewSessionForUser(dto)
const sessionCreationResult = await this.sessionService.createNewSessionForUser(dto)
try {
await this.domainEventPublisher.publish(
@@ -79,6 +87,6 @@ export class AuthResponseFactory20200115 extends AuthResponseFactory20190520 {
this.logger.error(`Failed to publish session created event: ${(error as Error).message}`)
}
return session
return sessionCreationResult
}
}

View File

@@ -1,3 +1,4 @@
import { Session } from '../Session/Session'
import { User } from '../User/User'
import { AuthResponse20161215 } from './AuthResponse20161215'
import { AuthResponse20200115 } from './AuthResponse20200115'
@@ -9,5 +10,5 @@ export interface AuthResponseFactoryInterface {
userAgent: string
ephemeralSession: boolean
readonlyAccess: boolean
}): Promise<AuthResponse20161215 | AuthResponse20200115>
}): Promise<{ response: AuthResponse20161215 | AuthResponse20200115; session?: Session }>
}

View File

@@ -40,7 +40,7 @@ describe('AuthenticationMethodResolver', () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.getSessionFromToken = jest.fn()
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
sessionService.getRevokedSessionFromToken = jest.fn()
sessionService.markRevokedSessionAsReceived = jest.fn().mockReturnValue(revokedSession)
@@ -70,7 +70,7 @@ describe('AuthenticationMethodResolver', () => {
})
it('should resolve session authentication method', async () => {
sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
expect(await createResolver().resolve('test')).toEqual({
session,
@@ -80,7 +80,9 @@ describe('AuthenticationMethodResolver', () => {
})
it('should not resolve session authentication method with invalid user uuid on session', async () => {
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ userUuid: 'invalid' })
sessionService.getSessionFromToken = jest
.fn()
.mockReturnValue({ session: { userUuid: 'invalid' }, isEphemeral: false })
expect(await createResolver().resolve('test')).toBeUndefined
})

View File

@@ -43,7 +43,7 @@ export class AuthenticationMethodResolver implements AuthenticationMethodResolve
}
}
const session = await this.sessionService.getSessionFromToken(token)
const { session } = await this.sessionService.getSessionFromToken(token)
if (session) {
this.logger.debug('Token decoded successfully. Session found.')

View File

@@ -1,150 +0,0 @@
import 'reflect-metadata'
import { FileRemovedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { User } from '../User/User'
import { FileRemovedEventHandler } from './FileRemovedEventHandler'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
describe('FileRemovedEventHandler', () => {
let userSubscriptionService: UserSubscriptionServiceInterface
let logger: Logger
let regularUser: User
let sharedUser: User
let event: FileRemovedEvent
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createHandler = () => new FileRemovedEventHandler(userSubscriptionService, subscriptionSettingService, logger)
beforeEach(() => {
regularUser = {
uuid: '123',
} as jest.Mocked<User>
sharedUser = {
uuid: '234',
} as jest.Mocked<User>
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(regularUser),
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(sharedUser),
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
subscriptionSettingService.createOrReplace = jest.fn()
event = {} as jest.Mocked<FileRemovedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
fileByteSize: 123,
filePath: '1-2-3/2-3-4',
fileName: '2-3-4',
regularSubscriptionUuid: '4-5-6',
}
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
it('should do nothing a bytes used setting does not exist', async () => {
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
it('should not do anything if a user subscription is not found', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
it('should update a bytes used setting', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user: regularUser,
userSubscription: {
uuid: '1-2-3',
subscriptionType: 'regular',
user: Promise.resolve(regularUser),
},
})
})
it('should update a bytes used setting on both shared and regular subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(1, {
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user: regularUser,
userSubscription: {
uuid: '1-2-3',
subscriptionType: 'regular',
user: Promise.resolve(regularUser),
},
})
expect(subscriptionSettingService.createOrReplace).toHaveBeenNthCalledWith(2, {
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user: sharedUser,
userSubscription: {
uuid: '2-3-4',
subscriptionType: 'shared',
user: Promise.resolve(sharedUser),
},
})
})
})

View File

@@ -1,63 +1,19 @@
import { DomainEventHandlerInterface, FileRemovedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
@injectable()
export class FileRemovedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.Auth_SubscriptionSettingService)
private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: FileRemovedEvent): Promise<void> {
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(event.payload.userUuid)
if (regularSubscription === null) {
this.logger.warn(`Could not find regular user subscription for user with uuid: ${event.payload.userUuid}`)
return
}
await this.updateUploadBytesUsedSetting(regularSubscription, event.payload.fileByteSize)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, event.payload.fileByteSize)
}
}
private async updateUploadBytesUsedSetting(subscription: UserSubscription, byteSize: number): Promise<void> {
const user = await subscription.user
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: user.uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.userUuid,
bytesUsed: -event.payload.fileByteSize,
})
if (bytesUsedSetting === null) {
this.logger.warn(`Could not find bytes used setting for user with uuid: ${user.uuid}`)
return
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
const bytesUsed = bytesUsedSetting.value as string
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed - byteSize).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
}
}

View File

@@ -1,82 +1,19 @@
import { DomainEventHandlerInterface, FileUploadedEvent } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { EncryptionVersion } from '../Encryption/EncryptionVersion'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { User } from '../User/User'
import { Uuid } from '@standardnotes/domain-core'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
@injectable()
export class FileUploadedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.Auth_SubscriptionSettingService)
private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: FileUploadedEvent): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(userUuidOrError.getError())
return
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
return
}
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
if (regularSubscription === null) {
this.logger.warn(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
return
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, event.payload.fileByteSize)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, event.payload.fileByteSize)
}
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
byteSize: number,
): Promise<void> {
let bytesUsed = '0'
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.userUuid,
bytesUsed: event.payload.fileByteSize,
})
if (bytesUsedSetting !== null) {
bytesUsed = bytesUsedSetting.value as string
}
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesUsed + byteSize).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
import { DomainEventHandlerInterface, SharedVaultFileRemovedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
export class SharedVaultFileRemovedEventHandler implements DomainEventHandlerInterface {
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: SharedVaultFileRemovedEvent): Promise<void> {
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.vaultOwnerUuid,
bytesUsed: -event.payload.fileByteSize,
})
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
}
}

View File

@@ -0,0 +1,19 @@
import { DomainEventHandlerInterface, SharedVaultFileUploadedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
export class SharedVaultFileUploadedEventHandler implements DomainEventHandlerInterface {
constructor(private updateStorageQuotaUsedForUserUseCase: UpdateStorageQuotaUsedForUser, private logger: Logger) {}
async handle(event: SharedVaultFileUploadedEvent): Promise<void> {
const result = await this.updateStorageQuotaUsedForUserUseCase.execute({
userUuid: event.payload.vaultOwnerUuid,
bytesUsed: event.payload.fileByteSize,
})
if (result.isFailed()) {
this.logger.error(`Failed to update storage quota used for user: ${result.getError()}`)
}
}
}

View File

@@ -4,13 +4,6 @@ export interface EphemeralSessionRepositoryInterface {
findOneByUuid(uuid: string): Promise<EphemeralSession | null>
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<EphemeralSession | null>
findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>>
updateTokensAndExpirationDates(
uuid: string,
hashedAccessToken: string,
hashedRefreshToken: string,
accessExpiration: Date,
refreshExpiration: Date,
): Promise<void>
deleteOne(uuid: string, userUuid: string): Promise<void>
save(ephemeralSession: EphemeralSession): Promise<void>
}

View File

@@ -1,3 +1,5 @@
import { Uuid } from '@standardnotes/domain-core'
import { Session } from './Session'
export interface SessionRepositoryInterface {
@@ -5,10 +7,8 @@ export interface SessionRepositoryInterface {
findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<Session | null>
findAllByRefreshExpirationAndUserUuid(userUuid: string): Promise<Array<Session>>
findAllByUserUuid(userUuid: string): Promise<Array<Session>>
deleteAllByUserUuid(userUuid: string, currentSessionUuid: string): Promise<void>
deleteAllByUserUuidExceptOne(dto: { userUuid: Uuid; currentSessionUuid: Uuid }): Promise<void>
deleteOneByUuid(uuid: string): Promise<void>
updateHashedTokens(uuid: string, hashedAccessToken: string, hashedRefreshToken: string): Promise<void>
updatedTokenExpirationDates(uuid: string, accessExpiration: Date, refreshExpiration: Date): Promise<void>
save(session: Session): Promise<Session>
remove(session: Session): Promise<Session>
clearUserAgentByUserUuid(userUuid: string): Promise<void>

View File

@@ -24,8 +24,8 @@ describe('SessionService', () => {
let sessionRepository: SessionRepositoryInterface
let ephemeralSessionRepository: EphemeralSessionRepositoryInterface
let revokedSessionRepository: RevokedSessionRepositoryInterface
let session: Session
let ephemeralSession: EphemeralSession
let existingSession: Session
let existingEphemeralSession: EphemeralSession
let revokedSession: RevokedSession
let settingService: SettingServiceInterface
let deviceDetector: UAParser
@@ -54,14 +54,14 @@ describe('SessionService', () => {
)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '2e1e43'
session.userUuid = '1-2-3'
session.userAgent = 'Chrome'
session.apiVersion = ApiVersion.v20200115
session.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
session.readonlyAccess = false
existingSession = {} as jest.Mocked<Session>
existingSession.uuid = '2e1e43'
existingSession.userUuid = '1-2-3'
existingSession.userAgent = 'Chrome'
existingSession.apiVersion = ApiVersion.v20200115
existingSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingSession.readonlyAccess = false
revokedSession = {} as jest.Mocked<RevokedSession>
revokedSession.uuid = '2e1e43'
@@ -69,9 +69,7 @@ describe('SessionService', () => {
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
sessionRepository.deleteOneByUuid = jest.fn()
sessionRepository.save = jest.fn().mockReturnValue(session)
sessionRepository.updateHashedTokens = jest.fn()
sessionRepository.updatedTokenExpirationDates = jest.fn()
sessionRepository.save = jest.fn().mockReturnValue(existingSession)
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
@@ -79,17 +77,18 @@ describe('SessionService', () => {
ephemeralSessionRepository = {} as jest.Mocked<EphemeralSessionRepositoryInterface>
ephemeralSessionRepository.save = jest.fn()
ephemeralSessionRepository.findOneByUuid = jest.fn()
ephemeralSessionRepository.updateTokensAndExpirationDates = jest.fn()
ephemeralSessionRepository.deleteOne = jest.fn()
revokedSessionRepository = {} as jest.Mocked<RevokedSessionRepositoryInterface>
revokedSessionRepository.save = jest.fn()
ephemeralSession = {} as jest.Mocked<EphemeralSession>
ephemeralSession.uuid = '2-3-4'
ephemeralSession.userAgent = 'Mozilla Firefox'
ephemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
ephemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingEphemeralSession = {} as jest.Mocked<EphemeralSession>
existingEphemeralSession.uuid = '2-3-4'
existingEphemeralSession.userUuid = '1-2-3'
existingEphemeralSession.userAgent = 'Mozilla Firefox'
existingEphemeralSession.hashedAccessToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingEphemeralSession.hashedRefreshToken = '4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce'
existingEphemeralSession.readonlyAccess = false
timer = {} as jest.Mocked<TimerInterface>
timer.convertStringDateToMilliseconds = jest.fn().mockReturnValue(123)
@@ -138,7 +137,7 @@ describe('SessionService', () => {
})
it('should refresh access and refresh tokens for a session', async () => {
expect(await createService().refreshTokens(session)).toEqual({
expect(await createService().refreshTokens({ session: existingSession, isEphemeral: false })).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_token: expect.any(String),
@@ -146,15 +145,28 @@ describe('SessionService', () => {
readonly_access: false,
})
expect(sessionRepository.updateHashedTokens).toHaveBeenCalled()
expect(sessionRepository.updatedTokenExpirationDates).toHaveBeenCalled()
expect(sessionRepository.save).toHaveBeenCalled()
expect(ephemeralSessionRepository.save).not.toHaveBeenCalled()
})
it('should refresh access and refresh tokens for an ephemeral session', async () => {
expect(await createService().refreshTokens({ session: existingEphemeralSession, isEphemeral: true })).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_token: expect.any(String),
refresh_expiration: 123,
readonly_access: false,
})
expect(sessionRepository.save).not.toHaveBeenCalled()
expect(ephemeralSessionRepository.save).toHaveBeenCalled()
})
it('should create new session for a user', async () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -176,7 +188,7 @@ describe('SessionService', () => {
readonlyAccess: false,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -190,7 +202,7 @@ describe('SessionService', () => {
user.email = 'demo@standardnotes.com'
user.uuid = '123'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -212,7 +224,7 @@ describe('SessionService', () => {
readonlyAccess: true,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -229,7 +241,7 @@ describe('SessionService', () => {
value: LogSessionUserAgentOption.Disabled,
} as jest.Mocked<Setting>)
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -250,7 +262,7 @@ describe('SessionService', () => {
readonlyAccess: false,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -305,7 +317,7 @@ describe('SessionService', () => {
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -317,7 +329,7 @@ describe('SessionService', () => {
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -333,7 +345,7 @@ describe('SessionService', () => {
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -345,7 +357,7 @@ describe('SessionService', () => {
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -361,7 +373,7 @@ describe('SessionService', () => {
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
const result = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -373,7 +385,7 @@ describe('SessionService', () => {
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -386,7 +398,7 @@ describe('SessionService', () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'
const sessionPayload = await createService().createNewEphemeralSessionForUser({
const result = await createService().createNewEphemeralSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
@@ -408,7 +420,7 @@ describe('SessionService', () => {
readonlyAccess: false,
})
expect(sessionPayload).toEqual({
expect(result.sessionHttpRepresentation).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
@@ -420,7 +432,7 @@ describe('SessionService', () => {
it('should delete a session by token', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
@@ -429,13 +441,28 @@ describe('SessionService', () => {
await createService().deleteSessionByToken('1:2:3')
expect(sessionRepository.deleteOneByUuid).toHaveBeenCalledWith('2e1e43')
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2e1e43', '1-2-3')
expect(ephemeralSessionRepository.deleteOne).not.toHaveBeenCalled()
})
it('should delete an ephemeral session by token', async () => {
ephemeralSessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return existingEphemeralSession
}
return null
})
await createService().deleteSessionByToken('1:2:3')
expect(sessionRepository.deleteOneByUuid).not.toHaveBeenCalled()
expect(ephemeralSessionRepository.deleteOne).toHaveBeenCalledWith('2-3-4', '1-2-3')
})
it('should not delete a session by token if session is not found', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
@@ -448,13 +475,13 @@ describe('SessionService', () => {
})
it('should determine if a refresh token is valid', async () => {
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:3')).toBeTruthy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2:4')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(session, '1:2')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:3')).toBeTruthy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2:4')).toBeFalsy()
expect(createService().isRefreshTokenMatchingHashedSessionToken(existingSession, '1:2')).toBeFalsy()
})
it('should return device info based on user agent', () => {
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Mac 10.13')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Mac 10.13')
})
it('should return device info based on undefined user agent', () => {
@@ -463,7 +490,7 @@ describe('SessionService', () => {
browser: { name: undefined, version: undefined },
os: { name: undefined, version: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
})
it('should return a shorter info based on lack of client in user agent', () => {
@@ -473,7 +500,7 @@ describe('SessionService', () => {
os: { name: 'iOS', version: '10.3' },
})
expect(createService().getDeviceInfo(session)).toEqual('iOS 10.3')
expect(createService().getDeviceInfo(existingSession)).toEqual('iOS 10.3')
})
it('should return a shorter info based on lack of os in user agent', () => {
@@ -483,13 +510,13 @@ describe('SessionService', () => {
os: { name: '', version: '' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0')
})
it('should return unknown client and os if user agent is cleaned out', () => {
session.userAgent = null
existingSession.userAgent = null
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
})
it('should return a shorter info based on partial os in user agent', () => {
@@ -499,7 +526,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on Windows')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on Windows')
deviceDetector.getResult = jest.fn().mockReturnValue({
ua: 'dummy-data',
@@ -507,7 +534,7 @@ describe('SessionService', () => {
os: { name: '', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome 69.0 on 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome 69.0 on 7')
})
it('should return a shorter info based on partial client in user agent', () => {
@@ -517,7 +544,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows 7')
deviceDetector.getResult = jest.fn().mockReturnValue({
ua: 'dummy-data',
@@ -525,7 +552,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome on Windows 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on Windows 7')
})
it('should return a shorter info based on iOS agent', () => {
@@ -538,7 +565,7 @@ describe('SessionService', () => {
cpu: { architecture: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('iOS')
expect(createService().getDeviceInfo(existingSession)).toEqual('iOS')
})
it('should return a shorter info based on partial client and partial os in user agent', () => {
@@ -548,7 +575,7 @@ describe('SessionService', () => {
os: { name: 'Windows', version: '' },
})
expect(createService().getDeviceInfo(session)).toEqual('69.0 on Windows')
expect(createService().getDeviceInfo(existingSession)).toEqual('69.0 on Windows')
deviceDetector.getResult = jest.fn().mockReturnValue({
ua: 'dummy-data',
@@ -556,7 +583,7 @@ describe('SessionService', () => {
os: { name: '', version: '7' },
})
expect(createService().getDeviceInfo(session)).toEqual('Chrome on 7')
expect(createService().getDeviceInfo(existingSession)).toEqual('Chrome on 7')
})
it('should return only Android os for okHttp client', () => {
@@ -569,7 +596,7 @@ describe('SessionService', () => {
cpu: { architecture: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('Android')
expect(createService().getDeviceInfo(existingSession)).toEqual('Android')
})
it('should detect the StandardNotes app in user agent', () => {
@@ -582,7 +609,7 @@ describe('SessionService', () => {
cpu: { architecture: undefined },
})
expect(createService().getDeviceInfo(session)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
expect(createService().getDeviceInfo(existingSession)).toEqual('Standard Notes Desktop 3.5.18 on Mac OS 10.16.0')
})
it('should return unknown device info as fallback', () => {
@@ -590,70 +617,72 @@ describe('SessionService', () => {
throw new Error('something bad happened')
})
expect(createService().getDeviceInfo(session)).toEqual('Unknown Client on Unknown OS')
expect(createService().getDeviceInfo(existingSession)).toEqual('Unknown Client on Unknown OS')
})
it('should retrieve a session from a session token', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
})
const result = await createService().getSessionFromToken('1:2:3')
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
expect(result).toEqual(session)
expect(session).toEqual(session)
expect(isEphemeral).toBeFalsy()
})
it('should retrieve an ephemeral session from a session token', async () => {
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(ephemeralSession)
ephemeralSessionRepository.findOneByUuid = jest.fn().mockReturnValue(existingEphemeralSession)
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await createService().getSessionFromToken('1:2:3')
const { session, isEphemeral } = await createService().getSessionFromToken('1:2:3')
expect(result).toEqual(ephemeralSession)
expect(session).toEqual(existingEphemeralSession)
expect(isEphemeral).toBeTruthy()
})
it('should not retrieve a session from a session token that has access token missing', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
})
const result = await createService().getSessionFromToken('1:2')
const { session } = await createService().getSessionFromToken('1:2')
expect(result).toBeUndefined()
expect(session).toBeUndefined()
})
it('should not retrieve a session that is missing', async () => {
sessionRepository.findOneByUuid = jest.fn().mockReturnValue(null)
const result = await createService().getSessionFromToken('1:2:3')
const { session } = await createService().getSessionFromToken('1:2:3')
expect(result).toBeUndefined()
expect(session).toBeUndefined()
})
it('should not retrieve a session from a session token that has invalid access token', async () => {
sessionRepository.findOneByUuid = jest.fn().mockImplementation((uuid) => {
if (uuid === '2') {
return session
return existingSession
}
return null
})
const result = await createService().getSessionFromToken('1:2:4')
const { session } = await createService().getSessionFromToken('1:2:4')
expect(result).toBeUndefined()
expect(session).toBeUndefined()
})
it('should revoked a session', async () => {
await createService().createRevokedSession(session)
await createService().createRevokedSession(existingSession)
expect(revokedSessionRepository.save).toHaveBeenCalledWith({
uuid: '2e1e43',

View File

@@ -49,7 +49,7 @@ export class SessionService implements SessionServiceInterface {
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody> {
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
const session = await this.createSession({
ephemeral: false,
...dto,
@@ -73,7 +73,10 @@ export class SessionService implements SessionServiceInterface {
this.logger.error(`Could not trace session while creating cross service token.: ${(error as Error).message}`)
}
return sessionPayload
return {
sessionHttpRepresentation: sessionPayload,
session,
}
}
async createNewEphemeralSessionForUser(dto: {
@@ -81,7 +84,7 @@ export class SessionService implements SessionServiceInterface {
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody> {
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }> {
const ephemeralSession = await this.createSession({
ephemeral: true,
...dto,
@@ -91,27 +94,20 @@ export class SessionService implements SessionServiceInterface {
await this.ephemeralSessionRepository.save(ephemeralSession)
return sessionPayload
return {
sessionHttpRepresentation: sessionPayload,
session: ephemeralSession,
}
}
async refreshTokens(session: Session): Promise<SessionBody> {
const sessionPayload = await this.createTokens(session)
async refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody> {
const sessionPayload = await this.createTokens(dto.session)
await this.sessionRepository.updateHashedTokens(session.uuid, session.hashedAccessToken, session.hashedRefreshToken)
await this.sessionRepository.updatedTokenExpirationDates(
session.uuid,
session.accessExpiration,
session.refreshExpiration,
)
await this.ephemeralSessionRepository.updateTokensAndExpirationDates(
session.uuid,
session.hashedAccessToken,
session.hashedRefreshToken,
session.accessExpiration,
session.refreshExpiration,
)
if (dto.isEphemeral) {
await this.ephemeralSessionRepository.save(dto.session)
} else {
await this.sessionRepository.save(dto.session)
}
return sessionPayload
}
@@ -190,25 +186,25 @@ export class SessionService implements SessionServiceInterface {
return `${browserInfo} on ${osInfo}`
}
async getSessionFromToken(token: string): Promise<Session | undefined> {
async getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }> {
const tokenParts = token.split(':')
const sessionUuid = tokenParts[1]
const accessToken = tokenParts[2]
if (!accessToken) {
return undefined
return { session: undefined, isEphemeral: false }
}
const session = await this.getSession(sessionUuid)
const { session, isEphemeral } = await this.getSession(sessionUuid)
if (!session) {
return undefined
return { session: undefined, isEphemeral: false }
}
const hashedAccessToken = crypto.createHash('sha256').update(accessToken).digest('hex')
if (crypto.timingSafeEqual(Buffer.from(session.hashedAccessToken), Buffer.from(hashedAccessToken))) {
return session
return { session, isEphemeral }
}
return undefined
return { session: undefined, isEphemeral: false }
}
async getRevokedSessionFromToken(token: string): Promise<RevokedSession | null> {
@@ -229,11 +225,14 @@ export class SessionService implements SessionServiceInterface {
}
async deleteSessionByToken(token: string): Promise<string | null> {
const session = await this.getSessionFromToken(token)
const { session, isEphemeral } = await this.getSessionFromToken(token)
if (session) {
await this.sessionRepository.deleteOneByUuid(session.uuid)
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
if (isEphemeral) {
await this.ephemeralSessionRepository.deleteOne(session.uuid, session.userUuid)
} else {
await this.sessionRepository.deleteOneByUuid(session.uuid)
}
return session.userUuid
}
@@ -278,14 +277,19 @@ export class SessionService implements SessionServiceInterface {
return session
}
private async getSession(uuid: string): Promise<Session | null> {
private async getSession(uuid: string): Promise<{
session: Session | null
isEphemeral: boolean
}> {
let session = await this.ephemeralSessionRepository.findOneByUuid(uuid)
let isEphemeral = true
if (!session) {
session = await this.sessionRepository.findOneByUuid(uuid)
isEphemeral = false
}
return session
return { session, isEphemeral }
}
private async createTokens(session: Session): Promise<SessionBody> {

View File

@@ -9,15 +9,15 @@ export interface SessionServiceInterface {
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody>
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
createNewEphemeralSessionForUser(dto: {
user: User
apiVersion: string
userAgent: string
readonlyAccess: boolean
}): Promise<SessionBody>
refreshTokens(session: Session): Promise<SessionBody>
getSessionFromToken(token: string): Promise<Session | undefined>
}): Promise<{ sessionHttpRepresentation: SessionBody; session: Session }>
refreshTokens(dto: { session: Session; isEphemeral: boolean }): Promise<SessionBody>
getSessionFromToken(token: string): Promise<{ session: Session | undefined; isEphemeral: boolean }>
getRevokedSessionFromToken(token: string): Promise<RevokedSession | null>
markRevokedSessionAsReceived(revokedSession: RevokedSession): Promise<RevokedSession>
deleteSessionByToken(token: string): Promise<string | null>

View File

@@ -11,7 +11,10 @@ import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ChangeCredentials } from './ChangeCredentials'
import { Username } from '@standardnotes/domain-core'
import { Result, Username } from '@standardnotes/domain-core'
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
import { ApiVersion } from '../../Api/ApiVersion'
import { Session } from '../../Session/Session'
describe('ChangeCredentials', () => {
let userRepository: UserRepositoryInterface
@@ -21,13 +24,23 @@ describe('ChangeCredentials', () => {
let domainEventFactory: DomainEventFactoryInterface
let timer: TimerInterface
let user: User
let deleteOtherSessionsForUser: DeleteOtherSessionsForUser
const createUseCase = () =>
new ChangeCredentials(userRepository, authResponseFactoryResolver, domainEventPublisher, domainEventFactory, timer)
new ChangeCredentials(
userRepository,
authResponseFactoryResolver,
domainEventPublisher,
domainEventFactory,
timer,
deleteOtherSessionsForUser,
)
beforeEach(() => {
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: { uuid: '1-2-3' } as jest.Mocked<Session> })
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)
@@ -49,27 +62,25 @@ describe('ChangeCredentials', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
deleteOtherSessionsForUser = {} as jest.Mocked<DeleteOtherSessionsForUser>
deleteOtherSessionsForUser.execute = jest.fn().mockReturnValue(Result.ok())
})
it('should change password', async () => {
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: true,
authResponse: {
foo: 'bar',
},
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
pwNonce: 'asdzxc',
@@ -81,29 +92,24 @@ describe('ChangeCredentials', () => {
})
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalled()
})
it('should change email', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user).mockReturnValueOnce(null)
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: true,
authResponse: {
foo: 'bar',
},
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
@@ -116,6 +122,7 @@ describe('ChangeCredentials', () => {
})
expect(domainEventFactory.createUserEmailChangedEvent).toHaveBeenCalledWith('1-2-3', 'test@test.te', 'new@test.te')
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalled()
})
it('should not change email if already taken', async () => {
@@ -124,22 +131,19 @@ describe('ChangeCredentials', () => {
.mockReturnValueOnce(user)
.mockReturnValueOnce({} as jest.Mocked<User>)
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: false,
errorMessage: 'The email you entered is already taken. Please try again.',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: 'new@test.te',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('The email you entered is already taken. Please try again.')
expect(userRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
@@ -147,22 +151,19 @@ describe('ChangeCredentials', () => {
})
it('should not change email if the new email is invalid', async () => {
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: false,
errorMessage: 'Username cannot be empty',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Username cannot be empty')
expect(userRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
@@ -172,63 +173,52 @@ describe('ChangeCredentials', () => {
it('should not change email if the user is not found', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
}),
).toEqual({
success: false,
errorMessage: 'User not found.',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
newEmail: '',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('User not found.')
expect(userRepository.save).not.toHaveBeenCalled()
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should not change password if current password is incorrect', async () => {
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'test123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
}),
).toEqual({
success: false,
errorMessage: 'The current password you entered is incorrect. Please try again.',
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'test123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('The current password you entered is incorrect. Please try again.')
expect(userRepository.save).not.toHaveBeenCalled()
})
it('should update protocol version while changing password', async () => {
expect(
await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: '20190520',
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
protocolVersion: '004',
}),
).toEqual({
success: true,
authResponse: {
foo: 'bar',
},
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
protocolVersion: '004',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
@@ -239,4 +229,63 @@ describe('ChangeCredentials', () => {
updatedAt: new Date(1),
})
})
it('should not delete other sessions for user if neither passoword nor email are changed', async () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValueOnce(user)
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'qweqwe123123',
newEmail: undefined,
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
email: 'test@test.te',
uuid: '1-2-3',
pwNonce: 'asdzxc',
kpCreated: '123',
kpOrigination: 'password-change',
updatedAt: new Date(1),
})
expect(domainEventFactory.createUserEmailChangedEvent).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
})
it('should not delete other sessions for user if the caller does not support sessions', async () => {
authResponseFactory.createResponse = jest.fn().mockReturnValue({ response: { foo: 'bar' } })
const result = await createUseCase().execute({
username: Username.create('test@test.te').getValue(),
apiVersion: ApiVersion.v20200115,
currentPassword: 'qweqwe123123',
newPassword: 'test234',
pwNonce: 'asdzxc',
updatedWithUserAgent: 'Google Chrome',
kpCreated: '123',
kpOrigination: 'password-change',
})
expect(result.isFailed()).toBeFalsy()
expect(userRepository.save).toHaveBeenCalledWith({
encryptedPassword: expect.any(String),
pwNonce: 'asdzxc',
kpCreated: '123',
email: 'test@test.te',
uuid: '1-2-3',
kpOrigination: 'password-change',
updatedAt: new Date(1),
})
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
})
})

View File

@@ -1,20 +1,22 @@
import * as bcrypt from 'bcryptjs'
import { inject, injectable } from 'inversify'
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { AuthResponseFactoryResolverInterface } from '../../Auth/AuthResponseFactoryResolverInterface'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { ChangeCredentialsDTO } from './ChangeCredentialsDTO'
import { ChangeCredentialsResponse } from './ChangeCredentialsResponse'
import { UseCaseInterface } from '../UseCaseInterface'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface, UserEmailChangedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Username } from '@standardnotes/domain-core'
import { DeleteOtherSessionsForUser } from '../DeleteOtherSessionsForUser'
import { AuthResponse20161215 } from '../../Auth/AuthResponse20161215'
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
import { Session } from '../../Session/Session'
@injectable()
export class ChangeCredentials implements UseCaseInterface {
export class ChangeCredentials implements UseCaseInterface<AuthResponse20161215 | AuthResponse20200115> {
constructor(
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_AuthResponseFactoryResolver)
@@ -22,22 +24,18 @@ export class ChangeCredentials implements UseCaseInterface {
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_DeleteOtherSessionsForUser)
private deleteOtherSessionsForUserUseCase: DeleteOtherSessionsForUser,
) {}
async execute(dto: ChangeCredentialsDTO): Promise<ChangeCredentialsResponse> {
async execute(dto: ChangeCredentialsDTO): Promise<Result<AuthResponse20161215 | AuthResponse20200115>> {
const user = await this.userRepository.findOneByUsernameOrEmail(dto.username)
if (!user) {
return {
success: false,
errorMessage: 'User not found.',
}
return Result.fail('User not found.')
}
if (!(await bcrypt.compare(dto.currentPassword, user.encryptedPassword))) {
return {
success: false,
errorMessage: 'The current password you entered is incorrect. Please try again.',
}
return Result.fail('The current password you entered is incorrect. Please try again.')
}
user.encryptedPassword = await bcrypt.hash(dto.newPassword, User.PASSWORD_HASH_COST)
@@ -46,19 +44,13 @@ export class ChangeCredentials implements UseCaseInterface {
if (dto.newEmail !== undefined) {
const newUsernameOrError = Username.create(dto.newEmail)
if (newUsernameOrError.isFailed()) {
return {
success: false,
errorMessage: newUsernameOrError.getError(),
}
return Result.fail(newUsernameOrError.getError())
}
const newUsername = newUsernameOrError.getValue()
const existingUser = await this.userRepository.findOneByUsernameOrEmail(newUsername)
if (existingUser !== null) {
return {
success: false,
errorMessage: 'The email you entered is already taken. Please try again.',
}
return Result.fail('The email you entered is already taken. Please try again.')
}
userEmailChangedEvent = this.domainEventFactory.createUserEmailChangedEvent(
@@ -90,15 +82,35 @@ export class ChangeCredentials implements UseCaseInterface {
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
}),
const authResponse = await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
})
if (authResponse.session) {
await this.deleteOtherSessionsForUserIfNeeded(user.uuid, authResponse.session, dto)
}
return Result.ok(authResponse.response)
}
private async deleteOtherSessionsForUserIfNeeded(
userUuid: string,
session: Session,
dto: ChangeCredentialsDTO,
): Promise<void> {
const passwordHasChanged = dto.newPassword !== dto.currentPassword
const userEmailChanged = dto.newEmail !== undefined
if (passwordHasChanged || userEmailChanged) {
await this.deleteOtherSessionsForUserUseCase.execute({
userUuid,
currentSessionUuid: session.uuid,
markAsRevoked: false,
})
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
import 'reflect-metadata'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeleteOtherSessionsForUser } from './DeleteOtherSessionsForUser'
describe('DeleteOtherSessionsForUser', () => {
let sessionRepository: SessionRepositoryInterface
let sessionService: SessionServiceInterface
let session: Session
let currentSession: Session
const createUseCase = () => new DeleteOtherSessionsForUser(sessionRepository, sessionService)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '00000000-0000-0000-0000-000000000000'
currentSession = {} as jest.Mocked<Session>
currentSession.uuid = '00000000-0000-0000-0000-000000000001'
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.deleteAllByUserUuidExceptOne = jest.fn()
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session, currentSession])
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.createRevokedSession = jest.fn()
})
it('should delete all sessions except current for a given user', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
markAsRevoked: true,
})
expect(result.isFailed()).toBeFalsy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).toHaveBeenCalled()
expect(sessionService.createRevokedSession).toHaveBeenCalledWith(session)
expect(sessionService.createRevokedSession).not.toHaveBeenCalledWith(currentSession)
})
it('should delete all sessions except current for a given user without marking as revoked', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
markAsRevoked: false,
})
expect(result.isFailed()).toBeFalsy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).toHaveBeenCalled()
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
})
it('should not delete any sessions if the user uuid is invalid', async () => {
const result = await createUseCase().execute({
userUuid: 'invalid',
currentSessionUuid: '00000000-0000-0000-0000-000000000001',
markAsRevoked: true,
})
expect(result.isFailed()).toBeTruthy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).not.toHaveBeenCalled()
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
})
it('should not delete any sessions if the current session uuid is invalid', async () => {
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
currentSessionUuid: 'invalid',
markAsRevoked: true,
})
expect(result.isFailed()).toBeTruthy()
expect(sessionRepository.deleteAllByUserUuidExceptOne).not.toHaveBeenCalled()
expect(sessionService.createRevokedSession).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,46 @@
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeleteOtherSessionsForUserDTO } from './DeleteOtherSessionsForUserDTO'
@injectable()
export class DeleteOtherSessionsForUser implements UseCaseInterface<void> {
constructor(
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
) {}
async execute(dto: DeleteOtherSessionsForUserDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const currentSessionUuidOrError = Uuid.create(dto.currentSessionUuid)
if (currentSessionUuidOrError.isFailed()) {
return Result.fail(currentSessionUuidOrError.getError())
}
const currentSessionUuid = currentSessionUuidOrError.getValue()
const sessions = await this.sessionRepository.findAllByUserUuid(dto.userUuid)
if (dto.markAsRevoked) {
await Promise.all(
sessions.map(async (session: Session) => {
if (session.uuid !== currentSessionUuid.value) {
await this.sessionService.createRevokedSession(session)
}
}),
)
}
await this.sessionRepository.deleteAllByUserUuidExceptOne({ userUuid, currentSessionUuid })
return Result.ok()
}
}

View File

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

View File

@@ -1,39 +0,0 @@
import 'reflect-metadata'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeletePreviousSessionsForUser } from './DeletePreviousSessionsForUser'
describe('DeletePreviousSessionsForUser', () => {
let sessionRepository: SessionRepositoryInterface
let sessionService: SessionServiceInterface
let session: Session
let currentSession: Session
const createUseCase = () => new DeletePreviousSessionsForUser(sessionRepository, sessionService)
beforeEach(() => {
session = {} as jest.Mocked<Session>
session.uuid = '1-2-3'
currentSession = {} as jest.Mocked<Session>
currentSession.uuid = '2-3-4'
sessionRepository = {} as jest.Mocked<SessionRepositoryInterface>
sessionRepository.deleteAllByUserUuid = jest.fn()
sessionRepository.findAllByUserUuid = jest.fn().mockReturnValue([session, currentSession])
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.createRevokedSession = jest.fn()
})
it('should delete all sessions except current for a given user', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', currentSessionUuid: '2-3-4' })).toEqual({ success: true })
expect(sessionRepository.deleteAllByUserUuid).toHaveBeenCalledWith('1-2-3', '2-3-4')
expect(sessionService.createRevokedSession).toHaveBeenCalledWith(session)
expect(sessionService.createRevokedSession).not.toHaveBeenCalledWith(currentSession)
})
})

View File

@@ -1,32 +0,0 @@
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { Session } from '../Session/Session'
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DeletePreviousSessionsForUserDTO } from './DeletePreviousSessionsForUserDTO'
import { DeletePreviousSessionsForUserResponse } from './DeletePreviousSessionsForUserResponse'
import { UseCaseInterface } from './UseCaseInterface'
@injectable()
export class DeletePreviousSessionsForUser implements UseCaseInterface {
constructor(
@inject(TYPES.Auth_SessionRepository) private sessionRepository: SessionRepositoryInterface,
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
) {}
async execute(dto: DeletePreviousSessionsForUserDTO): Promise<DeletePreviousSessionsForUserResponse> {
const sessions = await this.sessionRepository.findAllByUserUuid(dto.userUuid)
await Promise.all(
sessions.map(async (session: Session) => {
if (session.uuid !== dto.currentSessionUuid) {
await this.sessionService.createRevokedSession(session)
}
}),
)
await this.sessionRepository.deleteAllByUserUuid(dto.userUuid, dto.currentSessionUuid)
return { success: true }
}
}

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ describe('RefreshSessionToken', () => {
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
sessionService.getSessionFromToken = jest.fn().mockReturnValue(session)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
sessionService.refreshTokens = jest.fn().mockReturnValue({
access_token: 'token1',
refresh_token: 'token2',
@@ -51,9 +51,10 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
expect(result).toEqual({
success: true,
@@ -74,9 +75,10 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(sessionService.refreshTokens).toHaveBeenCalledWith(session)
expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
expect(result).toEqual({
success: true,
@@ -90,11 +92,12 @@ describe('RefreshSessionToken', () => {
})
it('should not refresh a session token if session is not found', async () => {
sessionService.getSessionFromToken = jest.fn().mockReturnValue(null)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session: undefined, isEphemeral: false })
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(result).toEqual({
@@ -110,6 +113,7 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(result).toEqual({
@@ -125,6 +129,7 @@ describe('RefreshSessionToken', () => {
const result = await createUseCase().execute({
accessToken: '123',
refreshToken: '234',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
})
expect(result).toEqual({

View File

@@ -21,7 +21,7 @@ export class RefreshSessionToken {
) {}
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> {
const session = await this.sessionService.getSessionFromToken(dto.accessToken)
const { session, isEphemeral } = await this.sessionService.getSessionFromToken(dto.accessToken)
if (!session) {
return {
success: false,
@@ -46,7 +46,9 @@ export class RefreshSessionToken {
}
}
const sessionPayload = await this.sessionService.refreshTokens(session)
session.userAgent = dto.userAgent
const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
try {
await this.domainEventPublisher.publish(

View File

@@ -1,4 +1,5 @@
export type RefreshSessionTokenDTO = {
accessToken: string
refreshToken: string
userAgent: string
}

View File

@@ -10,6 +10,7 @@ import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Register } from './Register'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { Session } from '../Session/Session'
describe('Register', () => {
let userRepository: UserRepositoryInterface
@@ -32,7 +33,9 @@ describe('Register', () => {
roleRepository.findOneByName = jest.fn().mockReturnValue(null)
authResponseFactory = {} as jest.Mocked<AuthResponseFactory20200115>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: {} as jest.Mocked<Session> })
crypter = {} as jest.Mocked<CrypterInterface>
crypter.generateEncryptedUserServerKey = jest.fn().mockReturnValue('test')

View File

@@ -83,15 +83,17 @@ export class Register implements UseCaseInterface {
await this.settingService.applyDefaultSettingsUponRegistration(user)
const result = await this.authResponseFactory20200115.createResponse({
user,
apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession,
readonlyAccess: false,
})
return {
success: true,
authResponse: (await this.authResponseFactory20200115.createResponse({
user,
apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession,
readonlyAccess: false,
})) as AuthResponse20200115,
authResponse: result.response as AuthResponse20200115,
}
}
}

View File

@@ -13,6 +13,7 @@ import { SignIn } from './SignIn'
import { PKCERepositoryInterface } from '../User/PKCERepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { ProtocolVersion } from '@standardnotes/common'
import { Session } from '../Session/Session'
describe('SignIn', () => {
let user: User
@@ -50,7 +51,9 @@ describe('SignIn', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: {} as jest.Mocked<Session> })
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)

View File

@@ -95,15 +95,17 @@ export class SignIn implements UseCaseInterface {
await this.sendSignInEmailNotification(user, dto.userAgent)
const result = await authResponseFactory.createResponse({
user,
apiVersion: dto.apiVersion,
userAgent: dto.userAgent,
ephemeralSession: dto.ephemeralSession,
readonlyAccess: false,
})
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user,
apiVersion: dto.apiVersion,
userAgent: dto.userAgent,
ephemeralSession: dto.ephemeralSession,
readonlyAccess: false,
}),
authResponse: result.response,
}
}

View File

@@ -124,7 +124,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
await this.clearLoginAttempts.execute({ email: username.value })
return Result.ok(authResponse as AuthResponse20200115)
return Result.ok(authResponse.response as AuthResponse20200115)
}
private async validateCodeVerifier(codeVerifier: string): Promise<boolean> {

View File

@@ -1,28 +1,22 @@
import 'reflect-metadata'
import { UpdateStorageQuotaUsedForUser } from './UpdateStorageQuotaUsedForUser'
import { FileUploadedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { FileUploadedEventHandler } from './FileUploadedEventHandler'
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../Subscription/UserSubscriptionServiceInterface'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
describe('FileUploadedEventHandler', () => {
describe('UpdateStorageQuotaUsedForUser', () => {
let userRepository: UserRepositoryInterface
let userSubscriptionService: UserSubscriptionServiceInterface
let logger: Logger
let user: User
let event: FileUploadedEvent
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createHandler = () =>
new FileUploadedEventHandler(userRepository, userSubscriptionService, subscriptionSettingService, logger)
const createUseCase = () =>
new UpdateStorageQuotaUsedForUser(userRepository, userSubscriptionService, subscriptionSettingService)
beforeEach(() => {
user = {
@@ -52,23 +46,15 @@ describe('FileUploadedEventHandler', () => {
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
subscriptionSettingService.createOrReplace = jest.fn()
event = {} as jest.Mocked<FileUploadedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '00000000-0000-0000-0000-000000000000',
fileByteSize: 123,
filePath: '00000000-0000-0000-0000-000000000000/2-3-4',
fileName: '2-3-4',
}
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
it('should create a bytes used setting if one does not exist', async () => {
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'FILE_UPLOAD_BYTES_USED',
@@ -86,9 +72,11 @@ describe('FileUploadedEventHandler', () => {
})
it('should not do anything if a user uuid is invalid', async () => {
event.payload.userUuid = 'invalid'
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: 'invalid',
bytesUsed: 123,
})
expect(result.isFailed()).toBeTruthy()
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
@@ -96,7 +84,11 @@ describe('FileUploadedEventHandler', () => {
it('should not do anything if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeTruthy()
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
@@ -109,16 +101,24 @@ describe('FileUploadedEventHandler', () => {
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeTruthy()
expect(subscriptionSettingService.createOrReplace).not.toHaveBeenCalled()
})
it('should update a bytes used setting if one does exist', async () => {
it('should add bytes used setting if one does exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
@@ -136,6 +136,32 @@ describe('FileUploadedEventHandler', () => {
})
})
it('should subtract bytes used setting if one does exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: -123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {
name: 'FILE_UPLOAD_BYTES_USED',
sensitive: false,
unencryptedValue: '222',
serverEncryptionVersion: 0,
},
user,
userSubscription: {
uuid: '00000000-0000-0000-0000-000000000000',
subscriptionType: 'regular',
user: Promise.resolve(user),
},
})
})
it('should update a bytes used setting on both regular and shared subscription', async () => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
@@ -144,7 +170,11 @@ describe('FileUploadedEventHandler', () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue({
value: 345,
})
await createHandler().handle(event)
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
bytesUsed: 123,
})
expect(result.isFailed()).toBeFalsy()
expect(subscriptionSettingService.createOrReplace).toHaveBeenCalledWith({
props: {

View File

@@ -0,0 +1,72 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { SettingName } from '@standardnotes/settings'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
import { User } from '../../User/User'
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
constructor(
private userRepository: UserRepositoryInterface,
private userSubscriptionService: UserSubscriptionServiceInterface,
private subscriptionSettingService: SubscriptionSettingServiceInterface,
) {}
async execute(dto: UpdateStorageQuotaUsedForUserDTO): Promise<Result<void>> {
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(`Could not find user with uuid: ${userUuid.value}`)
}
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(userUuid.value)
if (regularSubscription === null) {
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
}
return Result.ok()
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
bytesUsed: number,
): Promise<void> {
let bytesAlreadyUsed = '0'
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
if (bytesUsedSetting !== null) {
bytesAlreadyUsed = bytesUsedSetting.value as string
}
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),
sensitive: false,
serverEncryptionVersion: EncryptionVersion.Unencrypted,
},
})
}
}

View File

@@ -0,0 +1,4 @@
export interface UpdateStorageQuotaUsedForUserDTO {
userUuid: string
bytesUsed: number
}

View File

@@ -8,6 +8,7 @@ import { AuthResponseFactoryInterface } from '../Auth/AuthResponseFactoryInterfa
import { AuthResponseFactoryResolverInterface } from '../Auth/AuthResponseFactoryResolverInterface'
import { UpdateUser } from './UpdateUser'
import { Session } from '../Session/Session'
describe('UpdateUser', () => {
let userRepository: UserRepositoryInterface
@@ -24,7 +25,9 @@ describe('UpdateUser', () => {
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(undefined)
authResponseFactory = {} as jest.Mocked<AuthResponseFactoryInterface>
authResponseFactory.createResponse = jest.fn().mockReturnValue({ foo: 'bar' })
authResponseFactory.createResponse = jest
.fn()
.mockReturnValue({ response: { foo: 'bar' }, session: {} as jest.Mocked<Session> })
authResponseFactoryResolver = {} as jest.Mocked<AuthResponseFactoryResolverInterface>
authResponseFactoryResolver.resolveAuthResponseFactoryVersion = jest.fn().mockReturnValue(authResponseFactory)

View File

@@ -23,15 +23,17 @@ export class UpdateUser implements UseCaseInterface {
const authResponseFactory = this.authResponseFactoryResolver.resolveAuthResponseFactoryVersion(dto.apiVersion)
const result = await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
})
return {
success: true,
authResponse: await authResponseFactory.createResponse({
user: updatedUser,
apiVersion: dto.apiVersion,
userAgent: dto.updatedWithUserAgent,
ephemeralSession: false,
readonlyAccess: false,
}),
authResponse: result.response,
}
}
}

View File

@@ -1,6 +1,6 @@
import 'reflect-metadata'
import { InversifyExpressAdminController } from './InversifyExpressAdminController'
import { AnnotatedAdminController } from './AnnotatedAdminController'
import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { UserRepositoryInterface } from '../../Domain/User/UserRepositoryInterface'
@@ -9,7 +9,7 @@ import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
describe('InversifyExpressAdminController', () => {
describe('AnnotatedAdminController', () => {
let deleteSetting: DeleteSetting
let userRepository: UserRepositoryInterface
let createSubscriptionToken: CreateSubscriptionToken
@@ -18,12 +18,7 @@ describe('InversifyExpressAdminController', () => {
let user: User
const createController = () =>
new InversifyExpressAdminController(
deleteSetting,
userRepository,
createSubscriptionToken,
createOfflineSubscriptionToken,
)
new AnnotatedAdminController(deleteSetting, userRepository, createSubscriptionToken, createOfflineSubscriptionToken)
beforeEach(() => {
user = {} as jest.Mocked<User>

View File

@@ -9,14 +9,14 @@ import {
results,
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HomeServerAdminController } from './HomeServer/HomeServerAdminController'
import { BaseAdminController } from './Base/BaseAdminController'
import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
import { UserRepositoryInterface } from '../../Domain/User/UserRepositoryInterface'
@controller('/admin')
export class InversifyExpressAdminController extends HomeServerAdminController {
export class AnnotatedAdminController extends BaseAdminController {
constructor(
@inject(TYPES.Auth_DeleteSetting) override doDeleteSetting: DeleteSetting,
@inject(TYPES.Auth_UserRepository) override userRepository: UserRepositoryInterface,

View File

@@ -16,10 +16,10 @@ import { Logger } from 'winston'
import { GetUserKeyParams } from '../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
import { AuthController } from '../../Controller/AuthController'
import { inject } from 'inversify'
import { HomeServerAuthController } from './HomeServer/HomeServerAuthController'
import { BaseAuthController } from './Base/BaseAuthController'
@controller('/auth')
export class InversifyExpressAuthController extends HomeServerAuthController {
export class AnnotatedAuthController extends BaseAuthController {
constructor(
@inject(TYPES.Auth_VerifyMFA) override verifyMFA: VerifyMFA,
@inject(TYPES.Auth_SignIn) override signInUseCase: SignIn,

View File

@@ -10,10 +10,10 @@ import {
import TYPES from '../../Bootstrap/Types'
import { AuthenticatorsController } from '../../Controller/AuthenticatorsController'
import { inject } from 'inversify'
import { HomeServerAuthenticatorsController } from './HomeServer/HomeServerAuthenticatorsController'
import { BaseAuthenticatorsController } from './Base/BaseAuthenticatorsController'
@controller('/authenticators')
export class InversifyExpressAuthenticatorsController extends HomeServerAuthenticatorsController {
export class AnnotatedAuthenticatorsController extends BaseAuthenticatorsController {
constructor(
@inject(TYPES.Auth_AuthenticatorsController) override authenticatorsController: AuthenticatorsController,
) {

View File

@@ -2,19 +2,19 @@ import 'reflect-metadata'
import * as express from 'express'
import { InversifyExpressFeaturesController } from './InversifyExpressFeaturesController'
import { AnnotatedFeaturesController } from './AnnotatedFeaturesController'
import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
describe('InversifyExpressFeaturesController', () => {
describe('AnnotatedFeaturesController', () => {
let getUserFeatures: GetUserFeatures
let request: express.Request
let response: express.Response
let user: User
const createController = () => new InversifyExpressFeaturesController(getUserFeatures)
const createController = () => new AnnotatedFeaturesController(getUserFeatures)
beforeEach(() => {
user = {} as jest.Mocked<User>

View File

@@ -8,10 +8,10 @@ import {
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { HomeServerFeaturesController } from './HomeServer/HomeServerFeaturesController'
import { BaseFeaturesController } from './Base/BaseFeaturesController'
@controller('/users/:userUuid/features')
export class InversifyExpressFeaturesController extends HomeServerFeaturesController {
export class AnnotatedFeaturesController extends BaseFeaturesController {
constructor(@inject(TYPES.Auth_GetUserFeatures) override doGetUserFeatures: GetUserFeatures) {
super(doGetUserFeatures)
}

View File

@@ -1,7 +1,7 @@
import { controller, httpGet } from 'inversify-express-utils'
@controller('/healthcheck')
export class InversifyExpressHealthCheckController {
export class AnnotatedHealthCheckController {
@httpGet('/')
public async get(): Promise<string> {
return 'OK'

View File

@@ -2,13 +2,13 @@ import 'reflect-metadata'
import * as express from 'express'
import { InversifyExpressInternalController } from './InversifyExpressInternalController'
import { AnnotatedInternalController } from './AnnotatedInternalController'
import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
describe('InversifyExpressInternalController', () => {
describe('AnnotatedInternalController', () => {
let getUserFeatures: GetUserFeatures
let getSetting: GetSetting
@@ -16,7 +16,7 @@ describe('InversifyExpressInternalController', () => {
let response: express.Response
let user: User
const createController = () => new InversifyExpressInternalController(getUserFeatures, getSetting)
const createController = () => new AnnotatedInternalController(getUserFeatures, getSetting)
beforeEach(() => {
user = {} as jest.Mocked<User>

View File

@@ -12,7 +12,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
@controller('/internal')
export class InversifyExpressInternalController extends BaseHttpController {
export class AnnotatedInternalController extends BaseHttpController {
constructor(
@inject(TYPES.Auth_GetUserFeatures) private doGetUserFeatures: GetUserFeatures,
@inject(TYPES.Auth_GetSetting) private doGetSetting: GetSetting,

View File

@@ -3,18 +3,18 @@ import 'reflect-metadata'
import * as express from 'express'
import { results } from 'inversify-express-utils'
import { InversifyExpressListedController } from './InversifyExpressListedController'
import { AnnotatedListedController } from './AnnotatedListedController'
import { User } from '../../Domain/User/User'
import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
describe('InversifyExpressListedController', () => {
describe('AnnotatedListedController', () => {
let createListedAccount: CreateListedAccount
let request: express.Request
let response: express.Response
let user: User
const createController = () => new InversifyExpressListedController(createListedAccount)
const createController = () => new AnnotatedListedController(createListedAccount)
beforeEach(() => {
user = {} as jest.Mocked<User>

View File

@@ -4,10 +4,10 @@ import { controller, httpPost, results } from 'inversify-express-utils'
import { Request, Response } from 'express'
import TYPES from '../../Bootstrap/Types'
import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
import { HomeServerListedController } from './HomeServer/HomeServerListedController'
import { BaseListedController } from './Base/BaseListedController'
@controller('/listed')
export class InversifyExpressListedController extends HomeServerListedController {
export class AnnotatedListedController extends BaseListedController {
constructor(@inject(TYPES.Auth_CreateListedAccount) override doCreateListedAccount: CreateListedAccount) {
super(doCreateListedAccount)
}

View File

@@ -2,7 +2,7 @@ import 'reflect-metadata'
import * as express from 'express'
import { InversifyExpressOfflineController } from './InversifyExpressOfflineController'
import { AnnotatedOfflineController } from './AnnotatedOfflineController'
import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
@@ -15,7 +15,7 @@ import { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/secu
import { SubscriptionName } from '@standardnotes/common'
import { Logger } from 'winston'
describe('InversifyExpressOfflineController', () => {
describe('AnnotatedOfflineController', () => {
let getUserFeatures: GetUserFeatures
let getUserOfflineSubscription: GetUserOfflineSubscription
let createOfflineSubscriptionToken: CreateOfflineSubscriptionToken
@@ -29,7 +29,7 @@ describe('InversifyExpressOfflineController', () => {
let user: User
const createController = () =>
new InversifyExpressOfflineController(
new AnnotatedOfflineController(
getUserFeatures,
getUserOfflineSubscription,
createOfflineSubscriptionToken,

View File

@@ -14,10 +14,10 @@ import { AuthenticateOfflineSubscriptionToken } from '../../Domain/UseCase/Authe
import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetUserOfflineSubscription } from '../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
import { HomeServerOfflineController } from './HomeServer/HomeServerOfflineController'
import { BaseOfflineController } from './Base/BaseOfflineController'
@controller('/offline')
export class InversifyExpressOfflineController extends HomeServerOfflineController {
export class AnnotatedOfflineController extends BaseOfflineController {
constructor(
@inject(TYPES.Auth_GetUserFeatures) override doGetUserFeatures: GetUserFeatures,
@inject(TYPES.Auth_GetUserOfflineSubscription) override getUserOfflineSubscription: GetUserOfflineSubscription,

View File

@@ -2,34 +2,35 @@ import 'reflect-metadata'
import * as express from 'express'
import { InversifyExpressSessionController } from './InversifyExpressSessionController'
import { AnnotatedSessionController } from './AnnotatedSessionController'
import { results } from 'inversify-express-utils'
import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
describe('InversifyExpressSessionController', () => {
describe('AnnotatedSessionController', () => {
let deleteSessionForUser: DeleteSessionForUser
let deletePreviousSessionsForUser: DeletePreviousSessionsForUser
let deleteOtherSessionsForUser: DeleteOtherSessionsForUser
let refreshSessionToken: RefreshSessionToken
let request: express.Request
let response: express.Response
const createController = () =>
new InversifyExpressSessionController(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
new AnnotatedSessionController(deleteSessionForUser, deleteOtherSessionsForUser, refreshSessionToken)
beforeEach(() => {
deleteSessionForUser = {} as jest.Mocked<DeleteSessionForUser>
deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: true })
deletePreviousSessionsForUser = {} as jest.Mocked<DeletePreviousSessionsForUser>
deletePreviousSessionsForUser.execute = jest.fn()
deleteOtherSessionsForUser = {} as jest.Mocked<DeleteOtherSessionsForUser>
deleteOtherSessionsForUser.execute = jest.fn()
refreshSessionToken = {} as jest.Mocked<RefreshSessionToken>
refreshSessionToken.execute = jest.fn()
request = {
body: {},
headers: {},
} as jest.Mocked<express.Request>
response = {
@@ -70,6 +71,7 @@ describe('InversifyExpressSessionController', () => {
it('should return bad request upon failed tokens refreshing', async () => {
request.body.access_token = '123'
request.body.refresh_token = '234'
request.headers['user-agent'] = 'Google Chrome'
refreshSessionToken.execute = jest.fn().mockReturnValue({
success: false,
@@ -196,9 +198,10 @@ describe('InversifyExpressSessionController', () => {
const httpResult = <results.JsonResult>await createController().deleteAllSessions(request, response)
const result = await httpResult.executeAsync()
expect(deletePreviousSessionsForUser.execute).toHaveBeenCalledWith({
expect(deleteOtherSessionsForUser.execute).toHaveBeenCalledWith({
userUuid: '123',
currentSessionUuid: '234',
markAsRevoked: true,
})
expect(result.statusCode).toEqual(204)
@@ -218,7 +221,7 @@ describe('InversifyExpressSessionController', () => {
const httpResponse = <results.JsonResult>await createController().deleteAllSessions(request, response)
const result = await httpResponse.executeAsync()
expect(deletePreviousSessionsForUser.execute).not.toHaveBeenCalled()
expect(deleteOtherSessionsForUser.execute).not.toHaveBeenCalled()
expect(result.statusCode).toEqual(401)
})

View File

@@ -8,20 +8,20 @@ import {
results,
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteOtherSessionsForUser } from '../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
import { HomeServerSessionController } from './HomeServer/HomeServerSessionController'
import { BaseSessionController } from './Base/BaseSessionController'
@controller('/session')
export class InversifyExpressSessionController extends HomeServerSessionController {
export class AnnotatedSessionController extends BaseSessionController {
constructor(
@inject(TYPES.Auth_DeleteSessionForUser) override deleteSessionForUser: DeleteSessionForUser,
@inject(TYPES.Auth_DeletePreviousSessionsForUser)
override deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
@inject(TYPES.Auth_DeleteOtherSessionsForUser)
override deleteOtherSessionsForUser: DeleteOtherSessionsForUser,
@inject(TYPES.Auth_RefreshSessionToken) override refreshSessionToken: RefreshSessionToken,
) {
super(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
super(deleteSessionForUser, deleteOtherSessionsForUser, refreshSessionToken)
}
@httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)

View File

@@ -2,7 +2,7 @@ import 'reflect-metadata'
import * as express from 'express'
import { InversifyExpressSessionsController } from './InversifyExpressSessionsController'
import { AnnotatedSessionsController } from './AnnotatedSessionsController'
import { results } from 'inversify-express-utils'
import { User } from '@standardnotes/responses'
@@ -12,7 +12,7 @@ import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessions
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Session } from '../../Domain/Session/Session'
describe('InversifyExpressSessionsController', () => {
describe('AnnotatedSessionsController', () => {
let getActiveSessionsForUser: GetActiveSessionsForUser
let authenticateRequest: AuthenticateRequest
let sessionProjector: ProjectorInterface<Session>
@@ -23,7 +23,7 @@ describe('InversifyExpressSessionsController', () => {
let createCrossServiceToken: CreateCrossServiceToken
const createController = () =>
new InversifyExpressSessionsController(
new AnnotatedSessionsController(
getActiveSessionsForUser,
authenticateRequest,
sessionProjector,

View File

@@ -14,10 +14,10 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Session } from '../../Domain/Session/Session'
import { HomeServerSessionsController } from './HomeServer/HomeServerSessionsController'
import { BaseSessionsController } from './Base/BaseSessionsController'
@controller('/sessions')
export class InversifyExpressSessionsController extends HomeServerSessionsController {
export class AnnotatedSessionsController extends BaseSessionsController {
constructor(
@inject(TYPES.Auth_GetActiveSessionsForUser) override getActiveSessionsForUser: GetActiveSessionsForUser,
@inject(TYPES.Auth_AuthenticateRequest) override authenticateRequest: AuthenticateRequest,

View File

@@ -2,7 +2,7 @@ import 'reflect-metadata'
import * as express from 'express'
import { InversifyExpressSettingsController } from './InversifyExpressSettingsController'
import { AnnotatedSettingsController } from './AnnotatedSettingsController'
import { results } from 'inversify-express-utils'
import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion'
import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
@@ -11,7 +11,7 @@ import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
import { User } from '../../Domain/User/User'
describe('InversifyExpressSettingsController', () => {
describe('AnnotatedSettingsController', () => {
let deleteSetting: DeleteSetting
let getSettings: GetSettings
let getSetting: GetSetting
@@ -21,8 +21,7 @@ describe('InversifyExpressSettingsController', () => {
let response: express.Response
let user: User
const createController = () =>
new InversifyExpressSettingsController(getSettings, getSetting, updateSetting, deleteSetting)
const createController = () => new AnnotatedSettingsController(getSettings, getSetting, updateSetting, deleteSetting)
beforeEach(() => {
deleteSetting = {} as jest.Mocked<DeleteSetting>

View File

@@ -13,10 +13,10 @@ import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
import { HomeServerSettingsController } from './HomeServer/HomeServerSettingsController'
import { BaseSettingsController } from './Base/BaseSettingsController'
@controller('/users/:userUuid')
export class InversifyExpressSettingsController extends HomeServerSettingsController {
export class AnnotatedSettingsController extends BaseSettingsController {
constructor(
@inject(TYPES.Auth_GetSettings) override doGetSettings: GetSettings,
@inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting,

View File

@@ -11,10 +11,10 @@ import { inject } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
import { HomeServerSubscriptionInvitesController } from './HomeServer/HomeServerSubscriptionInvitesController'
import { BaseSubscriptionInvitesController } from './Base/BaseSubscriptionInvitesController'
@controller('/subscription-invites')
export class InversifyExpressSubscriptionInvitesController extends HomeServerSubscriptionInvitesController {
export class AnnotatedSubscriptionInvitesController extends BaseSubscriptionInvitesController {
constructor(
@inject(TYPES.Auth_SubscriptionInvitesController)
override subscriptionInvitesController: SubscriptionInvitesController,

View File

@@ -3,18 +3,18 @@ import 'reflect-metadata'
import * as express from 'express'
import { results } from 'inversify-express-utils'
import { InversifyExpressSubscriptionSettingsController } from './InversifyExpressSubscriptionSettingsController'
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
import { User } from '../../Domain/User/User'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
describe('InversifyExpressSubscriptionSettingsController', () => {
describe('AnnotatedSubscriptionSettingsController', () => {
let getSetting: GetSetting
let request: express.Request
let response: express.Response
let user: User
const createController = () => new InversifyExpressSubscriptionSettingsController(getSetting)
const createController = () => new AnnotatedSubscriptionSettingsController(getSetting)
beforeEach(() => {
user = {} as jest.Mocked<User>

View File

@@ -8,10 +8,10 @@ import {
} from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { HomeServerSubscriptionSettingsController } from './HomeServer/HomeServerSubscriptionSettingsController'
import { BaseSubscriptionSettingsController } from './Base/BaseSubscriptionSettingsController'
@controller('/users/:userUuid')
export class InversifyExpressSubscriptionSettingsController extends HomeServerSubscriptionSettingsController {
export class AnnotatedSubscriptionSettingsController extends BaseSubscriptionSettingsController {
constructor(@inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting) {
super(doGetSetting)
}

View File

@@ -3,7 +3,7 @@ import 'reflect-metadata'
import * as express from 'express'
import { results } from 'inversify-express-utils'
import { InversifyExpressSubscriptionTokensController } from './InversifyExpressSubscriptionTokensController'
import { AnnotatedSubscriptionTokensController } from './AnnotatedSubscriptionTokensController'
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
import { Setting } from '../../Domain/Setting/Setting'
@@ -15,7 +15,7 @@ import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { User } from '../../Domain/User/User'
import { Role } from '../../Domain/Role/Role'
describe('InversifyExpressSubscriptionTokensController', () => {
describe('AnnotatedSubscriptionTokensController', () => {
let createSubscriptionToken: CreateSubscriptionToken
let authenticateToken: AuthenticateSubscriptionToken
const jwtTTL = 60
@@ -31,7 +31,7 @@ describe('InversifyExpressSubscriptionTokensController', () => {
let role: Role
const createController = () =>
new InversifyExpressSubscriptionTokensController(
new AnnotatedSubscriptionTokensController(
createSubscriptionToken,
authenticateToken,
settingService,

View File

@@ -15,10 +15,10 @@ import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/Authenticate
import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
import { User } from '../../Domain/User/User'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { HomeServerSubscriptionTokensController } from './HomeServer/HomeServerSubscriptionTokensController'
import { BaseSubscriptionTokensController } from './Base/BaseSubscriptionTokensController'
@controller('/subscription-tokens')
export class InversifyExpressSubscriptionTokensController extends HomeServerSubscriptionTokensController {
export class AnnotatedSubscriptionTokensController extends BaseSubscriptionTokensController {
constructor(
@inject(TYPES.Auth_CreateSubscriptionToken) override createSubscriptionToken: CreateSubscriptionToken,
@inject(TYPES.Auth_AuthenticateSubscriptionToken) override authenticateToken: AuthenticateSubscriptionToken,

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