Compare commits

..

66 Commits

Author SHA1 Message Date
standardci
b510284e01 chore(release): publish new version
- @standardnotes/analytics@2.25.16
 - @standardnotes/api-gateway@1.71.1
 - @standardnotes/auth-server@1.134.0
 - @standardnotes/domain-events-infra@1.12.13
 - @standardnotes/domain-events@2.116.0
 - @standardnotes/event-store@1.11.22
 - @standardnotes/files-server@1.22.0
 - @standardnotes/home-server@1.15.0
 - @standardnotes/revisions-server@1.26.10
 - @standardnotes/scheduler-server@1.20.26
 - @standardnotes/security@1.11.0
 - @standardnotes/syncing-server@1.83.0
 - @standardnotes/websockets-server@1.10.20
2023-08-23 06:47:38 +00:00
Karol Sójko
205a1ed637 feat: add handling file moving and updating storage quota (#705)
* feat: add handling file moving and updating storage quota

* fix: getting file metada when moving files

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

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

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

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

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

* fix: caching cross service token

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

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

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

* fix: add transition mode enabled switch in docker entrypoint

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

* fix: mongodb item repository binding

* fix: item backups service binding

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

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

* fix: typeorm annotations for mongodb entity

* wip mongo repo

* feat: add mongodb queries

* fix(syncing-server): env sample

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

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

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

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

* fix: remove foreign key migration
2023-08-16 13:00:16 +02:00
Karol Sójko
faee38bffd fix: hosts for home-server e2e ci setup 2023-08-15 13:17:20 +02:00
Karol Sójko
65f3503fe8 fix: docker compose ci setup 2023-08-15 13:11:14 +02:00
Karol Sójko
054023b791 fix: host variables 2023-08-15 12:59:13 +02:00
Karol Sójko
383c3a68fa fix: default value for SECONDARY_DB_ENABLED 2023-08-15 12:56:55 +02:00
Karol Sójko
7d22b1c15c feat: run mongo db secondary database in e2e 2023-08-15 12:50:38 +02:00
standardci
c71e7cd926 chore(release): publish new version
- @standardnotes/auth-server@1.131.5
 - @standardnotes/home-server@1.13.49
2023-08-15 10:34:11 +00:00
Karol Sójko
83ad069c5d fix(auth): passing the invalidate cache header (#697) 2023-08-15 12:16:01 +02:00
standardci
081108d9ba chore(release): publish new version
- @standardnotes/home-server@1.13.48
 - @standardnotes/syncing-server@1.78.11
2023-08-11 11:52:27 +00:00
Karol Sójko
8f3df56a2b chore: fix revisions frequency 2023-08-11 13:22:11 +02:00
Karol Sójko
d02124f4e5 Revert "tmp: disable shared vaults"
This reverts commit c49dc35ab5.
2023-08-11 12:28:57 +02:00
Karol Sójko
09e351fedb Revert "tmp: ci"
This reverts commit 06cedd11d8.
2023-08-11 12:27:12 +02:00
Karol Sójko
ad4b85b095 Revert "tmp: disable decorating with associations on revisions"
This reverts commit ac3646836c.
2023-08-11 12:26:44 +02:00
Karol Sójko
0bf7d8beae Revert "tmp: disable decorating items completely"
This reverts commit bc1c7a8ae1.
2023-08-11 12:25:35 +02:00
standardci
1ae7cca394 chore(release): publish new version
- @standardnotes/home-server@1.13.47
 - @standardnotes/syncing-server@1.78.10
2023-08-11 09:00:00 +00:00
Karol Sójko
bc1c7a8ae1 tmp: disable decorating items completely 2023-08-11 10:54:12 +02:00
standardci
c22c5e4584 chore(release): publish new version
- @standardnotes/home-server@1.13.46
 - @standardnotes/syncing-server@1.78.9
2023-08-11 08:46:28 +00:00
Karol Sójko
ac3646836c tmp: disable decorating with associations on revisions 2023-08-11 10:40:03 +02:00
standardci
7a31ab75d6 chore(release): publish new version
- @standardnotes/home-server@1.13.45
 - @standardnotes/syncing-server@1.78.8
2023-08-11 08:23:28 +00:00
Karol Sójko
c49dc35ab5 tmp: disable shared vaults 2023-08-11 10:15:55 +02:00
Karol Sójko
06cedd11d8 tmp: ci 2023-08-11 10:15:55 +02:00
standardci
f496376fb3 chore(release): publish new version
- @standardnotes/scheduler-server@1.20.22
2023-08-11 08:14:41 +00:00
Karol Sójko
091e2a57e8 fix(scheduler): adjust email backups encouraging email schedule (#695) 2023-08-11 09:35:51 +02:00
standardci
0d40ef6796 chore(release): publish new version
- @standardnotes/analytics@2.25.13
 - @standardnotes/auth-server@1.131.4
 - @standardnotes/common@1.50.1
 - @standardnotes/home-server@1.13.44
 - @standardnotes/revisions-server@1.26.7
 - @standardnotes/syncing-server@1.78.7
 - @standardnotes/websockets-server@1.10.17
2023-08-11 07:35:15 +00:00
Mo
1be33ba4c3 refactor: remove unused functions (#694)
* refactor: remove unused functions

* refactor: remove unused functions
2023-08-11 08:58:39 +02:00
Mo
aaeb311928 chore: reduce ci revisions timeout 2023-08-10 13:09:49 -05:00
standardci
a7a38c07ac chore(release): publish new version
- @standardnotes/home-server@1.13.43
 - @standardnotes/syncing-server@1.78.6
2023-08-10 11:37:24 +00:00
Karol Sójko
56f49752b4 fix(syncing-server): setting user uuid in notifications 2023-08-10 13:04:51 +02:00
Mo
892d8b6fe2 chore: update email template 2023-08-10 05:49:30 -05:00
standardci
cec2005436 chore(release): publish new version
- @standardnotes/analytics@2.25.12
 - @standardnotes/api-gateway@1.70.4
 - @standardnotes/auth-server@1.131.3
 - @standardnotes/domain-core@1.25.2
 - @standardnotes/event-store@1.11.19
 - @standardnotes/files-server@1.20.3
 - @standardnotes/home-server@1.13.42
 - @standardnotes/revisions-server@1.26.6
 - @standardnotes/scheduler-server@1.20.21
 - @standardnotes/settings@1.21.24
 - @standardnotes/syncing-server@1.78.5
 - @standardnotes/websockets-server@1.10.16
2023-08-09 16:31:35 +00:00
Karol Sójko
0eb86c0096 Revert "tmp: disable fetching shared vault items"
This reverts commit 18eddea6f8.
2023-08-09 18:01:16 +02:00
Karol Sójko
b8e39d76c1 Revert "tmp: skip ci"
This reverts commit f8c9e67063.
2023-08-09 18:01:09 +02:00
Karol Sójko
1c3ff526b7 Revert "Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)""
This reverts commit d261c81cd0.
2023-08-09 18:00:49 +02:00
standardci
373767248c chore(release): publish new version
- @standardnotes/home-server@1.13.41
 - @standardnotes/syncing-server@1.78.4
2023-08-09 15:47:05 +00:00
Karol Sójko
d7965b2748 fix(syncing-server): casting handlers 2023-08-09 17:40:48 +02:00
Karol Sójko
cbcd2ec87a Revert "Revert "fix(syncing-server): update storage quota used in a shared vault (#691)""
This reverts commit 66f9352a06.
2023-08-09 17:36:59 +02:00
standardci
c74d37fc48 chore(release): publish new version
- @standardnotes/home-server@1.13.40
 - @standardnotes/syncing-server@1.78.3
2023-08-09 15:29:30 +00:00
Karol Sójko
66f9352a06 Revert "fix(syncing-server): update storage quota used in a shared vault (#691)"
This reverts commit 3415cae093.
2023-08-09 17:21:59 +02:00
standardci
e5eef3aba0 chore(release): publish new version
- @standardnotes/analytics@2.25.11
 - @standardnotes/api-gateway@1.70.3
 - @standardnotes/auth-server@1.131.2
 - @standardnotes/domain-core@1.25.1
 - @standardnotes/event-store@1.11.18
 - @standardnotes/files-server@1.20.2
 - @standardnotes/home-server@1.13.39
 - @standardnotes/revisions-server@1.26.5
 - @standardnotes/scheduler-server@1.20.20
 - @standardnotes/settings@1.21.23
 - @standardnotes/syncing-server@1.78.2
 - @standardnotes/websockets-server@1.10.15
2023-08-09 14:51:38 +00:00
Karol Sójko
d261c81cd0 Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)"
This reverts commit 46867c1a4d.
2023-08-09 16:43:33 +02:00
standardci
634e3bbb67 chore(release): publish new version
- @standardnotes/home-server@1.13.38
 - @standardnotes/syncing-server@1.78.1
2023-08-09 14:41:32 +00:00
Karol Sójko
f8c9e67063 tmp: skip ci 2023-08-09 16:33:59 +02:00
Karol Sójko
18eddea6f8 tmp: disable fetching shared vault items 2023-08-09 16:17:53 +02:00
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
220 changed files with 4735 additions and 1572 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,11 @@ on:
jobs:
e2e:
name: (Docker) E2E Test Suite
strategy:
fail-fast: false
matrix:
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
runs-on: ubuntu-latest
services:
@@ -45,12 +50,23 @@ jobs:
env:
DB_TYPE: mysql
CACHE_TYPE: redis
SECONDARY_DB_ENABLED: ${{ matrix.secondary_db_enabled }}
TRANSITION_MODE_ENABLED: ${{ matrix.transition_mode_enabled }}
- name: Wait for server to start
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
- name: Define if vault tests are enabled
id: vaults
run: |
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
else
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
fi
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
- name: Show logs on failure
if: ${{ failure() }}
@@ -67,13 +83,8 @@ jobs:
matrix:
db_type: [mysql, sqlite]
cache_type: [redis, memory]
include:
- cache_type: redis
db_type: mysql
redis_port: 6380
- cache_type: redis
db_type: sqlite
redis_port: 6381
secondary_db_enabled: [true, false]
transition_mode_enabled: [true, false]
runs-on: ubuntu-latest
@@ -85,16 +96,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 +142,23 @@ jobs:
sed -i "s/VALET_TOKEN_SECRET=/VALET_TOKEN_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
echo "DB_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3307" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes_${{ matrix.cache_type }}" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=sqlite_${{ matrix.cache_type }}.db" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
echo "DB_SQLITE_DATABASE_PATH=homeserver.db" >> packages/home-server/.env
echo "DB_USERNAME=standardnotes" >> packages/home-server/.env
echo "DB_PASSWORD=standardnotes" >> packages/home-server/.env
echo "DB_TYPE=${{ matrix.db_type }}" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:${{ matrix.redis_port }}" >> packages/home-server/.env
echo "REDIS_URL=redis://localhost:6379" >> packages/home-server/.env
echo "CACHE_TYPE=${{ matrix.cache_type }}" >> packages/home-server/.env
echo "SECONDARY_DB_ENABLED=${{ matrix.secondary_db_enabled }}" >> packages/home-server/.env
echo "TRANSITION_MODE_ENABLED=${{ matrix.transition_mode_enabled }}" >> packages/home-server/.env
echo "MONGO_HOST=localhost" >> packages/home-server/.env
echo "MONGO_PORT=27017" >> packages/home-server/.env
echo "MONGO_DATABASE=standardnotes" >> packages/home-server/.env
echo "MONGO_USERNAME=standardnotes" >> packages/home-server/.env
echo "MONGO_PASSWORD=standardnotes" >> packages/home-server/.env
echo "FILES_SERVER_URL=http://localhost:3123" >> packages/home-server/.env
echo "E2E_TESTING=true" >> packages/home-server/.env
@@ -144,8 +170,17 @@ jobs:
- name: Wait for server to start
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
- name: Define if vault tests are enabled
id: vaults
run: |
if [ "${{ matrix.secondary_db_enabled }}" = "true" ] && [ "${{ matrix.transition_mode_enabled }}" = "true" ]; then
echo "vault-tests=enabled" >> $GITHUB_OUTPUT
else
echo "vault-tests=disabled" >> $GITHUB_OUTPUT
fi
- name: Run E2E Test Suite
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html
run: yarn dlx mocha-headless-chrome --timeout 1800000 -f http://localhost:9001/mocha/test.html?vaults=${{ steps.vaults.outputs.vault-tests }}
- name: Show logs on failure
if: ${{ failure() }}

230
.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"],\
@@ -5869,6 +5870,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/webidl-conversions", [\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/@types-webidl-conversions-npm-7.0.0-0903313151-86c337dc1e.zip/node_modules/@types/webidl-conversions/",\
"packageDependencies": [\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/whatwg-url", [\
["npm:8.2.2", {\
"packageLocation": "./.yarn/cache/@types-whatwg-url-npm-8.2.2-54c5c24e6c-25f20f5649.zip/node_modules/@types/whatwg-url/",\
"packageDependencies": [\
["@types/whatwg-url", "npm:8.2.2"],\
["@types/node", "npm:20.2.5"],\
["@types/webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/yargs", [\
["npm:17.0.24", {\
"packageLocation": "./.yarn/cache/@types-yargs-npm-17.0.24-b034cf1d8b-f7811cc0b9.zip/node_modules/@types/yargs/",\
@@ -7074,6 +7095,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bson", [\
["npm:5.4.0", {\
"packageLocation": "./.yarn/cache/bson-npm-5.4.0-2f854c8216-2c913a45c0.zip/node_modules/bson/",\
"packageDependencies": [\
["bson", "npm:5.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["buffer", [\
["npm:5.7.1", {\
"packageLocation": "./.yarn/cache/buffer-npm-5.7.1-513ef8259e-8e611bed4d.zip/node_modules/buffer/",\
@@ -11932,6 +11962,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["memory-pager", [\
["npm:1.5.0", {\
"packageLocation": "./.yarn/cache/memory-pager-npm-1.5.0-46e20e6c81-6b00ff499b.zip/node_modules/memory-pager/",\
"packageDependencies": [\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["meow", [\
["npm:8.1.2", {\
"packageLocation": "./.yarn/cache/meow-npm-8.1.2-bcfe48d4f3-e36c879078.zip/node_modules/meow/",\
@@ -12290,6 +12329,59 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mongodb", [\
["npm:5.7.0", {\
"packageLocation": "./.yarn/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "npm:5.7.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0", {\
"packageLocation": "./.yarn/__virtual__/mongodb-virtual-eb0cd47e23/0/cache/mongodb-npm-5.7.0-c5e415a2e7-23a291ffe7.zip/node_modules/mongodb/",\
"packageDependencies": [\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["@aws-sdk/credential-providers", null],\
["@mongodb-js/zstd", null],\
["@types/aws-sdk__credential-providers", null],\
["@types/kerberos", null],\
["@types/mongodb-client-encryption", null],\
["@types/mongodb-js__zstd", null],\
["@types/snappy", null],\
["bson", "npm:5.4.0"],\
["kerberos", null],\
["mongodb-client-encryption", null],\
["mongodb-connection-string-url", "npm:2.6.0"],\
["saslprep", "npm:1.0.3"],\
["snappy", null],\
["socks", "npm:2.7.1"]\
],\
"packagePeers": [\
"@aws-sdk/credential-providers",\
"@mongodb-js/zstd",\
"@types/aws-sdk__credential-providers",\
"@types/kerberos",\
"@types/mongodb-client-encryption",\
"@types/mongodb-js__zstd",\
"@types/snappy",\
"kerberos",\
"mongodb-client-encryption",\
"snappy"\
],\
"linkType": "HARD"\
}]\
]],\
["mongodb-connection-string-url", [\
["npm:2.6.0", {\
"packageLocation": "./.yarn/cache/mongodb-connection-string-url-npm-2.6.0-af011ba17f-8a9186dd1b.zip/node_modules/mongodb-connection-string-url/",\
"packageDependencies": [\
["mongodb-connection-string-url", "npm:2.6.0"],\
["@types/whatwg-url", "npm:8.2.2"],\
["whatwg-url", "npm:11.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ms", [\
["npm:2.0.0", {\
"packageLocation": "./.yarn/cache/ms-npm-2.0.0-9e1101a471-de027828fc.zip/node_modules/ms/",\
@@ -14249,6 +14341,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["saslprep", [\
["npm:1.0.3", {\
"packageLocation": "./.yarn/cache/saslprep-npm-1.0.3-8db649c346-23ebcda091.zip/node_modules/saslprep/",\
"packageDependencies": [\
["saslprep", "npm:1.0.3"],\
["sparse-bitfield", "npm:3.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["schema-utils", [\
["npm:3.1.2", {\
"packageLocation": "./.yarn/cache/schema-utils-npm-3.1.2-d97c6dc247-11d35f997e.zip/node_modules/schema-utils/",\
@@ -14604,6 +14706,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["sparse-bitfield", [\
["npm:3.0.3", {\
"packageLocation": "./.yarn/cache/sparse-bitfield-npm-3.0.3-cb80d0c89f-625ecdf6f4.zip/node_modules/sparse-bitfield/",\
"packageDependencies": [\
["sparse-bitfield", "npm:3.0.3"],\
["memory-pager", "npm:1.5.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["spawn-please", [\
["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/spawn-please-npm-2.0.1-265b6b5432-fe19a7ceb5.zip/node_modules/spawn-please/",\
@@ -15246,6 +15358,14 @@ const RAW_RUNTIME_STATE =
["tr46", "npm:0.0.3"]\
],\
"linkType": "HARD"\
}],\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/tr46-npm-3.0.0-e1ae1ea7c9-3a481676bf.zip/node_modules/tr46/",\
"packageDependencies": [\
["tr46", "npm:3.0.0"],\
["punycode", "npm:2.3.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["treeverse", [\
@@ -15757,6 +15877,98 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-13b6364fde/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.16"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\
["@types/better-sqlite3", null],\
["@types/google-cloud__spanner", null],\
["@types/hdb-pool", null],\
["@types/ioredis", null],\
["@types/mongodb", null],\
["@types/mssql", null],\
["@types/mysql2", null],\
["@types/oracledb", null],\
["@types/pg", null],\
["@types/pg-native", null],\
["@types/pg-query-stream", null],\
["@types/redis", null],\
["@types/sap__hana-client", null],\
["@types/sql.js", null],\
["@types/sqlite3", null],\
["@types/ts-node", null],\
["@types/typeorm-aurora-data-api-driver", null],\
["app-root-path", "npm:3.1.0"],\
["better-sqlite3", null],\
["buffer", "npm:6.0.3"],\
["chalk", "npm:4.1.2"],\
["cli-highlight", "npm:2.1.11"],\
["date-fns", "npm:2.30.0"],\
["debug", "virtual:ac3d8e680759ce54399273724d44e041d6c9b73454d191d411a8c44bb27e22f02aaf6ed9d3ad0ac1c298eac4833cff369c9c7b84c573016112c4f84be2cd8543#npm:4.3.4"],\
["dotenv", "npm:16.1.3"],\
["glob", "npm:8.1.0"],\
["hdb-pool", null],\
["ioredis", null],\
["mkdirp", "npm:2.1.6"],\
["mongodb", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:5.7.0"],\
["mssql", null],\
["mysql2", "npm:3.3.3"],\
["oracledb", null],\
["pg", null],\
["pg-native", null],\
["pg-query-stream", null],\
["redis", null],\
["reflect-metadata", "npm:0.1.13"],\
["sha.js", "npm:2.4.11"],\
["sql.js", null],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-node", null],\
["tslib", "npm:2.5.2"],\
["typeorm-aurora-data-api-driver", null],\
["uuid", "npm:9.0.0"],\
["yargs", "npm:17.7.2"]\
],\
"packagePeers": [\
"@google-cloud/spanner",\
"@sap/hana-client",\
"@types/better-sqlite3",\
"@types/google-cloud__spanner",\
"@types/hdb-pool",\
"@types/ioredis",\
"@types/mongodb",\
"@types/mssql",\
"@types/mysql2",\
"@types/oracledb",\
"@types/pg-native",\
"@types/pg-query-stream",\
"@types/pg",\
"@types/redis",\
"@types/sap__hana-client",\
"@types/sql.js",\
"@types/sqlite3",\
"@types/ts-node",\
"@types/typeorm-aurora-data-api-driver",\
"better-sqlite3",\
"hdb-pool",\
"ioredis",\
"mongodb",\
"mssql",\
"mysql2",\
"oracledb",\
"pg-native",\
"pg-query-stream",\
"pg",\
"redis",\
"sql.js",\
"sqlite3",\
"ts-node",\
"typeorm-aurora-data-api-driver"\
],\
"linkType": "HARD"\
}],\
["virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.16", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-fc9b7b780b/0/cache/typeorm-npm-0.3.16-5ac12a7afc-19803f935e.zip/node_modules/typeorm/",\
"packageDependencies": [\
@@ -16191,6 +16403,13 @@ const RAW_RUNTIME_STATE =
["webidl-conversions", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.0.0", {\
"packageLocation": "./.yarn/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-bdbe11c68c.zip/node_modules/webidl-conversions/",\
"packageDependencies": [\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["webpack", [\
@@ -16249,6 +16468,15 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["whatwg-url", [\
["npm:11.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-11.0.0-073529d93a-ee3a532bfb.zip/node_modules/whatwg-url/",\
"packageDependencies": [\
["whatwg-url", "npm:11.0.0"],\
["tr46", "npm:3.0.0"],\
["webidl-conversions", "npm:7.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/whatwg-url-npm-5.0.0-374fb45e60-bd0cc6b75b.zip/node_modules/whatwg-url/",\
"packageDependencies": [\

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

View File

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

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.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.15...@standardnotes/analytics@2.25.16) (2023-08-23)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.14...@standardnotes/analytics@2.25.15) (2023-08-22)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.13...@standardnotes/analytics@2.25.14) (2023-08-18)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.12...@standardnotes/analytics@2.25.13) (2023-08-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.11...@standardnotes/analytics@2.25.12) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.10...@standardnotes/analytics@2.25.11) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.9...@standardnotes/analytics@2.25.10) (2023-08-09)
**Note:** Version bump only for package @standardnotes/analytics
## [2.25.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.25.8...@standardnotes/analytics@2.25.9) (2023-08-08)
**Note:** Version bump only for package @standardnotes/analytics

View File

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

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.71.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.71.0...@standardnotes/api-gateway@1.71.1) (2023-08-23)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.71.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.5...@standardnotes/api-gateway@1.71.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/api-gateway/issues/704)) ([34085ac](https://github.com/standardnotes/api-gateway/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
## [1.70.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.4...@standardnotes/api-gateway@1.70.5) (2023-08-18)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.3...@standardnotes/api-gateway@1.70.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.2...@standardnotes/api-gateway@1.70.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.1...@standardnotes/api-gateway@1.70.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.70.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.70.0...@standardnotes/api-gateway@1.70.1) (2023-08-08)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

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

View File

@@ -27,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
}
const authHeaderValue = request.headers.authorization as string
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
const cacheKey = `${authHeaderValue}${
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
}`
try {
let crossServiceTokenFetchedFromCache = true
let crossServiceToken = null
if (this.crossServiceTokenCacheTTL) {
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
}
if (crossServiceToken === null) {
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
const authResponse = await this.serviceProxy.validateSession({
authorization: authHeaderValue,
sharedVaultOwnerContext: sharedVaultOwnerContextHeaderValue,
})
if (!this.handleSessionValidationResponse(authResponse, response, next)) {
return
@@ -52,7 +59,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({
authorizationHeaderValue: authHeaderValue,
key: cacheKey,
encodedCrossServiceToken: crossServiceToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid,
@@ -62,6 +69,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
} catch (error) {
const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data)

View File

@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
constructor(private timer: TimerInterface) {}
async set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void> {
let userAuthHeaders = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userAuthHeadersJSON) {
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
let userKeys = []
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
if (userKeysJSON) {
userKeys = JSON.parse(userKeysJSON)
}
userAuthHeaders.push(dto.authorizationHeaderValue)
userKeys.push(dto.key)
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
}
async get(authorizationHeaderValue: string): Promise<string | null> {
async get(key: string): Promise<string | null> {
this.invalidateExpiredTokens()
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
if (!cachedToken) {
return null
}
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
}
async invalidate(userUuid: string): Promise<void> {
let userAuthorizationHeaderValues = []
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userAuthHeadersJSON) {
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
let userKeyValues = []
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
if (userKeysJSON) {
userKeyValues = JSON.parse(userKeysJSON)
}
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
for (const key of userKeyValues) {
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
}
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)

View File

@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
async set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void> {
const pipeline = this.redisClient.pipeline()
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
await pipeline.exec()
}
async get(authorizationHeaderValue: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
async get(key: string): Promise<string | null> {
return this.redisClient.get(`${this.PREFIX}:${key}`)
}
async invalidate(userUuid: string): Promise<void> {
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
const pipeline = this.redisClient.pipeline()
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
for (const key of userKeyValues) {
pipeline.del(`${this.PREFIX}:${key}`)
}
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)

View File

@@ -1,10 +1,10 @@
export interface CrossServiceTokenCacheInterface {
set(dto: {
authorizationHeaderValue: string
key: string
encodedCrossServiceToken: string
expiresAtInSeconds: number
userUuid: string
}): Promise<void>
get(authorizationHeaderValue: string): Promise<string | null>
get(key: string): Promise<string | null>
invalidate(userUuid: string): Promise<void>
}

View File

@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
) {}
async validateSession(
authorizationHeaderValue: string,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authResponse = await this.httpClient.request({
method: 'POST',
headers: {
Authorization: authorizationHeaderValue,
Authorization: headers.authorization,
Accept: 'application/json',
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
validateStatus: (status: number) => {
return status >= 200 && status < 500

View File

@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
endpointOrMethodIdentifier: string,
payload?: Record<string, unknown> | string,
): Promise<void>
validateSession(authorizationHeaderValue: string): Promise<{
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
status: number
data: unknown
headers: {

View File

@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
async validateSession(
authorizationHeaderValue: string,
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
async validateSession(headers: {
authorization: string
sharedVaultOwnerContext?: string
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
if (!authService) {
throw new Error('Auth service not found')
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
const serviceResponse = (await authService.handleRequest(
{
headers: {
authorization: authorizationHeaderValue,
authorization: headers.authorization,
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
},
} as never,
{} as never,

View File

@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.133.0...@standardnotes/auth-server@1.134.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.132.0...@standardnotes/auth-server@1.133.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/server/issues/704)) ([34085ac](https://github.com/standardnotes/server/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.5...@standardnotes/auth-server@1.132.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.131.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.4...@standardnotes/auth-server@1.131.5) (2023-08-15)
### Bug Fixes
* **auth:** passing the invalidate cache header ([#697](https://github.com/standardnotes/server/issues/697)) ([83ad069](https://github.com/standardnotes/server/commit/83ad069c5dd9afa3a6db881f0d8a55a58d0642aa))
## [1.131.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.3...@standardnotes/auth-server@1.131.4) (2023-08-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.2...@standardnotes/auth-server@1.131.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.1...@standardnotes/auth-server@1.131.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.131.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.131.0...@standardnotes/auth-server@1.131.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.131.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.130.1...@standardnotes/auth-server@1.131.0) (2023-08-08)
### Features

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTransitionRole1692348191367 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddTransitionRole1692348280258 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
)
}
public async down(): Promise<void> {
return
}
}

View File

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

View File

@@ -256,6 +256,7 @@ import { PaymentsAccountDeletedEventHandler } from '../Domain/Handler/PaymentsAc
import { UpdateStorageQuotaUsedForUser } from '../Domain/UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
import { SharedVaultFileUploadedEventHandler } from '../Domain/Handler/SharedVaultFileUploadedEventHandler'
import { SharedVaultFileRemovedEventHandler } from '../Domain/Handler/SharedVaultFileRemovedEventHandler'
import { SharedVaultFileMovedEventHandler } from '../Domain/Handler/SharedVaultFileMovedEventHandler'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -560,6 +561,9 @@ export class ContainerConfigLoader {
container
.bind(TYPES.Auth_READONLY_USERS)
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
container
.bind(TYPES.Auth_TRANSITION_MODE_ENABLED)
.toConstantValue(env.get('TRANSITION_MODE_ENABLED', true) === 'true')
if (isConfiguredForInMemoryCache) {
container
@@ -979,6 +983,14 @@ export class ContainerConfigLoader {
container.get(TYPES.Auth_Logger),
),
)
container
.bind<SharedVaultFileMovedEventHandler>(TYPES.Auth_SharedVaultFileMovedEventHandler)
.toConstantValue(
new SharedVaultFileMovedEventHandler(
container.get(TYPES.Auth_UpdateStorageQuotaUsedForUser),
container.get(TYPES.Auth_Logger),
),
)
container
.bind<FileRemovedEventHandler>(TYPES.Auth_FileRemovedEventHandler)
.toConstantValue(
@@ -1042,6 +1054,7 @@ export class ContainerConfigLoader {
['USER_EMAIL_CHANGED', container.get(TYPES.Auth_UserEmailChangedEventHandler)],
['FILE_UPLOADED', container.get(TYPES.Auth_FileUploadedEventHandler)],
['SHARED_VAULT_FILE_UPLOADED', container.get(TYPES.Auth_SharedVaultFileUploadedEventHandler)],
['SHARED_VAULT_FILE_MOVED', container.get(TYPES.Auth_SharedVaultFileMovedEventHandler)],
['FILE_REMOVED', container.get(TYPES.Auth_FileRemovedEventHandler)],
['SHARED_VAULT_FILE_REMOVED', container.get(TYPES.Auth_SharedVaultFileRemovedEventHandler)],
['LISTED_ACCOUNT_CREATED', container.get(TYPES.Auth_ListedAccountCreatedEventHandler)],

View File

@@ -27,6 +27,7 @@ export class Service implements AuthServiceInterface {
async activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>> {
if (!this.container) {

View File

@@ -101,6 +101,7 @@ const TYPES = {
Auth_U2F_EXPECTED_ORIGIN: Symbol.for('Auth_U2F_EXPECTED_ORIGIN'),
Auth_U2F_REQUIRE_USER_VERIFICATION: Symbol.for('Auth_U2F_REQUIRE_USER_VERIFICATION'),
Auth_READONLY_USERS: Symbol.for('Auth_READONLY_USERS'),
Auth_TRANSITION_MODE_ENABLED: Symbol.for('Auth_TRANSITION_MODE_ENABLED'),
// use cases
Auth_AuthenticateUser: Symbol.for('Auth_AuthenticateUser'),
Auth_AuthenticateRequest: Symbol.for('Auth_AuthenticateRequest'),
@@ -167,6 +168,7 @@ const TYPES = {
Auth_UserEmailChangedEventHandler: Symbol.for('Auth_UserEmailChangedEventHandler'),
Auth_FileUploadedEventHandler: Symbol.for('Auth_FileUploadedEventHandler'),
Auth_SharedVaultFileUploadedEventHandler: Symbol.for('Auth_SharedVaultFileUploadedEventHandler'),
Auth_SharedVaultFileMovedEventHandler: Symbol.for('Auth_SharedVaultFileMovedEventHandler'),
Auth_FileRemovedEventHandler: Symbol.for('Auth_FileRemovedEventHandler'),
Auth_SharedVaultFileRemovedEventHandler: Symbol.for('Auth_SharedVaultFileRemovedEventHandler'),
Auth_ListedAccountCreatedEventHandler: Symbol.for('Auth_ListedAccountCreatedEventHandler'),

View File

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

View File

@@ -57,7 +57,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
await this.subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription(
subscription,
new Map([[SettingName.NAMES.FileUploadBytesLimit, '-1']]),
new Map([[SettingName.NAMES.FileUploadBytesLimit, `${dto.uploadBytesLimit ?? -1}`]]),
)
return Result.ok('Premium features activated.')

View File

@@ -1,5 +1,6 @@
export interface ActivatePremiumFeaturesDTO {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}

View File

@@ -8,6 +8,8 @@ import { Role } from '../../Role/Role'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { GetSetting } from '../GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User>
@@ -15,6 +17,7 @@ describe('CreateCrossServiceToken', () => {
let roleProjector: ProjectorInterface<Role>
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface
let getSettingUseCase: GetSetting
const jwtTTL = 60
let session: Session
@@ -22,7 +25,15 @@ describe('CreateCrossServiceToken', () => {
let role: Role
const createUseCase = () =>
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
new CreateCrossServiceToken(
userProjector,
sessionProjector,
roleProjector,
tokenEncoder,
userRepository,
jwtTTL,
getSettingUseCase,
)
beforeEach(() => {
session = {} as jest.Mocked<Session>
@@ -50,6 +61,9 @@ describe('CreateCrossServiceToken', () => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
getSettingUseCase = {} as jest.Mocked<GetSetting>
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ setting: { value: '100' } }))
})
it('should create a cross service token for user', async () => {
@@ -125,28 +139,74 @@ describe('CreateCrossServiceToken', () => {
it('should throw an error if user does not exist', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
let caughtError = null
try {
await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
} catch (error) {
caughtError = error
}
const result = await createUseCase().execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(caughtError).not.toBeNull()
expect(result.isFailed()).toBeTruthy()
})
it('should throw an error if user uuid is invalid', async () => {
let caughtError = null
try {
await createUseCase().execute({
userUuid: 'invalid',
})
} catch (error) {
caughtError = error
}
const result = await createUseCase().execute({
userUuid: 'invalid',
})
expect(caughtError).not.toBeNull()
expect(result.isFailed()).toBeTruthy()
})
describe('shared vault context', () => {
it('should add shared vault context if shared vault owner uuid is provided', async () => {
await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
shared_vault_owner_context: {
upload_bytes_limit: 100,
},
user: {
email: 'test@test.te',
uuid: '00000000-0000-0000-0000-000000000000',
},
},
60,
)
})
it('should throw an error if shared vault owner context is sensitive', async () => {
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.ok({ sensitive: true }))
const result = await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
it('should throw an error if it fails to retrieve shared vault owner setting', async () => {
getSettingUseCase.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const result = await createUseCase().execute({
user,
session,
sharedVaultOwnerContext: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeTruthy()
})
})
})

View File

@@ -1,5 +1,6 @@
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
@@ -7,14 +8,13 @@ import { Role } from '../../Role/Role'
import { Session } from '../../Session/Session'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
import { Uuid } from '@standardnotes/domain-core'
import { GetSetting } from '../GetSetting/GetSetting'
import { SettingName } from '@standardnotes/settings'
@injectable()
export class CreateCrossServiceToken implements UseCaseInterface {
export class CreateCrossServiceToken implements UseCaseInterface<string> {
constructor(
@inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
@inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
@@ -22,14 +22,16 @@ export class CreateCrossServiceToken implements UseCaseInterface {
@inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
@inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
@inject(TYPES.Auth_GetSetting)
private getSettingUseCase: GetSetting,
) {}
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
let user: User | undefined | null = dto.user
if (user === undefined && dto.userUuid !== undefined) {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
throw new Error(userUuidOrError.getError())
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
@@ -37,7 +39,7 @@ export class CreateCrossServiceToken implements UseCaseInterface {
}
if (!user) {
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
return Result.fail(`Could not find user with uuid ${dto.userUuid}`)
}
const roles = await user.roles
@@ -45,15 +47,33 @@ export class CreateCrossServiceToken implements UseCaseInterface {
const authTokenData: CrossServiceTokenData = {
user: this.projectUser(user),
roles: this.projectRoles(roles),
shared_vault_owner_context: undefined,
}
if (dto.sharedVaultOwnerContext !== undefined) {
const uploadBytesLimitSettingOrError = await this.getSettingUseCase.execute({
settingName: SettingName.NAMES.FileUploadBytesLimit,
userUuid: dto.sharedVaultOwnerContext,
})
if (uploadBytesLimitSettingOrError.isFailed()) {
return Result.fail(uploadBytesLimitSettingOrError.getError())
}
const uploadBytesLimitSetting = uploadBytesLimitSettingOrError.getValue()
if (uploadBytesLimitSetting.sensitive) {
return Result.fail('Shared vault owner upload bytes limit setting is sensitive!')
}
const uploadBytesLimit = parseInt(uploadBytesLimitSetting.setting.value as string)
authTokenData.shared_vault_owner_context = {
upload_bytes_limit: uploadBytesLimit,
}
}
if (dto.session !== undefined) {
authTokenData.session = this.projectSession(dto.session)
}
return {
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
}
return Result.ok(this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL))
}
private projectUser(user: User): { uuid: string; email: string } {

View File

@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
{
user: User
session?: Session
sharedVaultOwnerContext?: string
},
{
userUuid: string

View File

@@ -1,3 +0,0 @@
export type CreateCrossServiceTokenResponse = {
token: string
}

View File

@@ -73,35 +73,30 @@ describe('GetSetting', () => {
describe('no subscription', () => {
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.DropboxBackupFrequency,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
it('should not find a setting if the setting name is invalid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
success: false,
error: {
message: 'Invalid setting name: invalid',
},
})
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
expect(result.isFailed()).toBeTruthy()
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: false,
error: {
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.DropboxBackupFrequency,
})
expect(result.isFailed()).toBeTruthy()
})
it('should not retrieve a sensitive setting for user', async () => {
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
success: true,
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true,
})
})
it('should not retrieve a subscription setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'No subscription found.',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeTruthy()
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
it('should not get a suscription setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
},
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeTruthy()
})
it('should not retrieve a sensitive subscription setting for user', async () => {
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
sensitive: true,
})
})
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
})
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MuteSignInEmails,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
})
it('should find a regular subscription only setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
).toEqual({
success: true,
const result = await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.FileUploadBytesLimit,
})
expect(result.isFailed()).toBeFalsy()
expect(result.getValue()).toEqual({
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})

View File

@@ -1,7 +1,7 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
import { UserSubscription } from '../../Subscription/UserSubscription'
@injectable()
export class GetSetting implements UseCaseInterface {
export class GetSetting implements UseCaseInterface<GetSettingResponse> {
constructor(
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
) {}
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
async execute(dto: GetSettingDto): Promise<Result<GetSettingResponse>> {
const settingNameOrError = SettingName.create(dto.settingName)
if (settingNameOrError.isFailed()) {
return {
success: false,
error: {
message: settingNameOrError.getError(),
},
}
return Result.fail(settingNameOrError.getError())
}
const settingName = settingNameOrError.getValue()
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
}
if (!subscription) {
return {
success: false,
error: {
message: 'No subscription found.',
},
}
return Result.fail('No subscription found.')
}
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
})
if (subscriptionSetting === null) {
return {
success: false,
error: {
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
}
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
return Result.ok({
sensitive: true,
}
})
}
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
return {
success: true,
return Result.ok({
userUuid: dto.userUuid,
setting: simpleSubscriptionSetting,
}
})
}
const setting = await this.settingService.findSettingWithDecryptedValue({
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
})
if (setting === null) {
return {
success: false,
error: {
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
}
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
return Result.ok({
sensitive: true,
}
})
}
const simpleSetting = await this.settingProjector.projectSimple(setting)
return {
success: true,
return Result.ok({
userUuid: dto.userUuid,
setting: simpleSetting,
}
})
}
}

View File

@@ -1,18 +1,13 @@
import { Either } from '@standardnotes/common'
import { SimpleSetting } from '../../Setting/SimpleSetting'
export type GetSettingResponse =
| {
success: true
userUuid: string
setting: SimpleSetting
}
| {
success: true
sensitive: true
}
| {
success: false
error: {
message: string
}
}
export type GetSettingResponse = Either<
{
userUuid: string
setting: SimpleSetting
},
{
sensitive: true
}
>

View File

@@ -11,6 +11,7 @@ import { Register } from './Register'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { Session } from '../Session/Session'
import { RoleName } from '@standardnotes/domain-core'
describe('Register', () => {
let userRepository: UserRepositoryInterface
@@ -20,9 +21,19 @@ describe('Register', () => {
let user: User
let crypter: CrypterInterface
let timer: TimerInterface
let transitionModeEnabled = false
const createUseCase = () =>
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
new Register(
userRepository,
roleRepository,
authResponseFactory,
crypter,
false,
settingService,
timer,
transitionModeEnabled,
)
beforeEach(() => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
@@ -75,6 +86,7 @@ describe('Register', () => {
updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String),
version: '004',
roles: Promise.resolve([]),
createdAt: new Date(1),
updatedAt: new Date(1),
})
@@ -118,6 +130,48 @@ describe('Register', () => {
})
})
it('should register a new user with default role and transition role', async () => {
transitionModeEnabled = true
const role = new Role()
role.name = RoleName.NAMES.CoreUser
const transitionRole = new Role()
transitionRole.name = RoleName.NAMES.TransitionUser
roleRepository.findOneByName = jest.fn().mockReturnValueOnce(role).mockReturnValueOnce(transitionRole)
expect(
await createUseCase().execute({
email: 'test@test.te',
password: 'asdzxc',
updatedWithUserAgent: 'Mozilla',
apiVersion: '20200115',
ephemeralSession: false,
version: '004',
pwCost: 11,
pwSalt: 'qweqwe',
pwNonce: undefined,
}),
).toEqual({ success: true, authResponse: { foo: 'bar' } })
expect(userRepository.save).toHaveBeenCalledWith({
email: 'test@test.te',
encryptedPassword: expect.any(String),
encryptedServerKey: 'test',
serverEncryptionVersion: 1,
pwCost: 11,
pwNonce: undefined,
pwSalt: 'qweqwe',
updatedWithUserAgent: 'Mozilla',
uuid: expect.any(String),
version: '004',
createdAt: new Date(1),
updatedAt: new Date(1),
roles: Promise.resolve([role, transitionRole]),
})
})
it('should fail to register if username is invalid', async () => {
expect(
await createUseCase().execute({
@@ -195,6 +249,7 @@ describe('Register', () => {
true,
settingService,
timer,
transitionModeEnabled,
).execute({
email: 'test@test.te',
password: 'asdzxc',

View File

@@ -1,8 +1,9 @@
import * as bcrypt from 'bcryptjs'
import { RoleName, Username } from '@standardnotes/domain-core'
import { v4 as uuidv4 } from 'uuid'
import { inject, injectable } from 'inversify'
import { TimerInterface } from '@standardnotes/time'
import TYPES from '../../Bootstrap/Types'
import { User } from '../User/User'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
@@ -11,7 +12,6 @@ import { RegisterResponse } from './RegisterResponse'
import { UseCaseInterface } from './UseCaseInterface'
import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
import { CrypterInterface } from '../Encryption/CrypterInterface'
import { TimerInterface } from '@standardnotes/time'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
@@ -27,6 +27,7 @@ export class Register implements UseCaseInterface {
@inject(TYPES.Auth_DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
@inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
@inject(TYPES.Auth_TRANSITION_MODE_ENABLED) private transitionModeEnabled: boolean,
) {}
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
@@ -72,10 +73,18 @@ export class Register implements UseCaseInterface {
user.encryptedServerKey = await this.crypter.generateEncryptedUserServerKey()
user.serverEncryptionVersion = User.DEFAULT_ENCRYPTION_VERSION
const roles = []
const defaultRole = await this.roleRepository.findOneByName(RoleName.NAMES.CoreUser)
if (defaultRole) {
user.roles = Promise.resolve([defaultRole])
roles.push(defaultRole)
}
if (this.transitionModeEnabled) {
const transitionRole = await this.roleRepository.findOneByName(RoleName.NAMES.TransitionUser)
if (transitionRole) {
roles.push(transitionRole)
}
}
user.roles = Promise.resolve(roles)
Object.assign(user, registrationFields)

View File

@@ -7,7 +7,6 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { UpdateStorageQuotaUsedForUserDTO } from './UpdateStorageQuotaUsedForUserDTO'
import { User } from '../../User/User'
export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
constructor(
@@ -34,23 +33,20 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
return Result.fail(`Could not find regular user subscription for user with uuid: ${userUuid.value}`)
}
await this.updateUploadBytesUsedSetting(regularSubscription, user, dto.bytesUsed)
await this.updateUploadBytesUsedSetting(regularSubscription, dto.bytesUsed)
if (sharedSubscription !== null) {
await this.updateUploadBytesUsedSetting(sharedSubscription, user, dto.bytesUsed)
await this.updateUploadBytesUsedSetting(sharedSubscription, dto.bytesUsed)
}
return Result.ok()
}
private async updateUploadBytesUsedSetting(
subscription: UserSubscription,
user: User,
bytesUsed: number,
): Promise<void> {
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
let bytesAlreadyUsed = '0'
const subscriptionUser = await subscription.user
const bytesUsedSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: (await subscription.user).uuid,
userUuid: subscriptionUser.uuid,
userSubscriptionUuid: subscription.uuid,
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(),
})
@@ -60,7 +56,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
await this.subscriptionSettingService.createOrReplace({
userSubscription: subscription,
user,
user: subscriptionUser,
props: {
name: SettingName.NAMES.FileUploadBytesUsed,
unencryptedValue: (+bytesAlreadyUsed + bytesUsed).toString(),

View File

@@ -7,6 +7,7 @@ import { results } from 'inversify-express-utils'
import { User } from '../../Domain/User/User'
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedInternalController', () => {
let getUserFeatures: GetUserFeatures
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar'
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync()
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
request.params.userUuid = '1-2-3'
request.params.settingName = 'foobar'
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSetting(request)
const result = await httpResponse.executeAsync()

View File

@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
@httpGet('/users/:userUuid/settings/:settingName')
async getSetting(request: Request): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
const resultOrError = await this.doGetSetting.execute({
userUuid: request.params.userUuid,
settingName: request.params.settingName,
allowSensitiveRetrieval: true,
})
if (result.success) {
return this.json(result)
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
}

View File

@@ -11,6 +11,7 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Session } from '../../Domain/Session/Session'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSessionsController', () => {
let getActiveSessionsForUser: GetActiveSessionsForUser
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
request = {
params: {},

View File

@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
import { User } from '../../Domain/User/User'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSettingsController', () => {
let deleteSetting: DeleteSetting
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
const result = await httpResponse.executeAsync()

View File

@@ -6,6 +6,7 @@ import { results } from 'inversify-express-utils'
import { AnnotatedSubscriptionSettingsController } from './AnnotatedSubscriptionSettingsController'
import { User } from '../../Domain/User/User'
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
import { Result } from '@standardnotes/domain-core'
describe('AnnotatedSubscriptionSettingsController', () => {
let getSetting: GetSetting
@@ -41,7 +42,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: true })
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()
@@ -58,7 +59,7 @@ describe('AnnotatedSubscriptionSettingsController', () => {
uuid: '1-2-3',
}
getSetting.execute = jest.fn().mockReturnValue({ success: false })
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
const httpResponse = <results.JsonResult>await createController().getSubscriptionSetting(request, response)
const result = await httpResponse.executeAsync()

View File

@@ -285,6 +285,10 @@ export class BaseAuthController extends BaseHttpController {
authorizationHeader: <string>request.headers.authorization,
})
if (result.headers?.has('x-invalidate-cache')) {
response.setHeader('x-invalidate-cache', result.headers.get('x-invalidate-cache') as string)
}
return this.json(result.data, result.status)
}

View File

@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
const user = authenticateRequestResponse.user as User
const result = await this.createCrossServiceToken.execute({
const sharedVaultOwnerContext = request.headers['x-shared-vault-owner-context'] as string | undefined
const resultOrError = await this.createCrossServiceToken.execute({
user,
session: authenticateRequestResponse.session,
sharedVaultOwnerContext,
})
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json({ authToken: result.token })
return this.json({ authToken: resultOrError.getValue() })
}
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {

View File

@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
}
const { userUuid, settingName } = request.params
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (result.success) {
return this.json(result)
const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {

View File

@@ -14,15 +14,25 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
}
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const result = await this.doGetSetting.execute({
const resultOrError = await this.doGetSetting.execute({
userUuid: response.locals.user.uuid,
settingName: request.params.subscriptionSettingName.toUpperCase(),
})
if (result.success) {
return this.json(result)
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json(result, 400)
return this.json({
success: true,
...resultOrError.getValue(),
})
}
}

View File

@@ -46,10 +46,20 @@ export class BaseWebSocketsController extends BaseHttpController {
)
}
const result = await this.createCrossServiceToken.execute({
const resultOrError = await this.createCrossServiceToken.execute({
userUuid: token.userUuid,
})
if (resultOrError.isFailed()) {
return this.json(
{
error: {
message: resultOrError.getError(),
},
},
400,
)
}
return this.json({ authToken: result.token })
return this.json({ authToken: resultOrError.getValue() })
}
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.50.1](https://github.com/standardnotes/server/compare/@standardnotes/common@1.50.0...@standardnotes/common@1.50.1) (2023-08-11)
**Note:** Version bump only for package @standardnotes/common
# [1.50.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.49.0...@standardnotes/common@1.50.0) (2023-07-12)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/common",
"version": "1.50.0",
"version": "1.50.1",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -1 +0,0 @@
export type ApplicationIdentifier = string

View File

@@ -5,43 +5,12 @@ export enum ProtocolVersion {
V004 = '004',
}
export const ProtocolVersionLatest = ProtocolVersion.V004
/** The last protocol version to not use root-key based items keys */
export const ProtocolVersionLastNonrootItemsKey = ProtocolVersion.V003
export const ProtocolExpirationDates: Partial<Record<ProtocolVersion, number>> = Object.freeze({
[ProtocolVersion.V001]: Date.parse('2018-01-01'),
[ProtocolVersion.V002]: Date.parse('2020-01-01'),
})
export function isProtocolVersionExpired(version: ProtocolVersion) {
const expireDate = ProtocolExpirationDates[version]
if (!expireDate) {
return false
}
const expired = new Date().getTime() > expireDate
return expired
}
export const ProtocolVersionLength = 3
export function protocolVersionFromEncryptedString(string: string): ProtocolVersion {
const version = string.substring(0, ProtocolVersionLength) as ProtocolVersion
if (Object.values(ProtocolVersion).includes(version)) {
return version
}
throw Error(`Unrecognized protocol version ${version}`)
}
/**
* -1 if a < b
* 0 if a == b
* 1 if a > b
*/
export function compareVersions(a: ProtocolVersion, b: ProtocolVersion): number {
function compareVersions(a: ProtocolVersion, b: ProtocolVersion): number {
const aNum = Number(a)
const bNum = Number(b)
return aNum - bNum
@@ -50,7 +19,3 @@ export function compareVersions(a: ProtocolVersion, b: ProtocolVersion): number
export function leftVersionGreaterThanOrEqualToRight(a: ProtocolVersion, b: ProtocolVersion): boolean {
return compareVersions(a, b) >= 0
}
export function isVersionLessThanOrEqualTo(input: ProtocolVersion, compareTo: ProtocolVersion): boolean {
return compareVersions(input, compareTo) <= 0
}

View File

@@ -3,7 +3,6 @@ export * from './Content/ContentDecoderInterface'
export * from './DataType/AnyRecord'
export * from './DataType/JSONString'
export * from './DataType/MicrosecondsTimestamp'
export * from './DataType/ApplicationIdentifier'
export * from './Email/EmailMessageIdentifier'
export * from './KeyParams/AnyKeyParamsContent'
export * from './KeyParams/BaseKeyParams'

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.26.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.2...@standardnotes/domain-core@1.26.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.25.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.1...@standardnotes/domain-core@1.25.2) (2023-08-09)
### Reverts
* Revert "Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)"" ([1c3ff52](https://github.com/standardnotes/server/commit/1c3ff526b7c4885f71f019f6c01142f522a6f8ad)), closes [#692](https://github.com/standardnotes/server/issues/692)
## [1.25.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.25.0...@standardnotes/domain-core@1.25.1) (2023-08-09)
### Reverts
* Revert "feat(syncing-server): notify shared vault users upon file uploads or removals (#692)" ([d261c81](https://github.com/standardnotes/server/commit/d261c81cd0bdbb9001c8589224f007ed2d338903)), closes [#692](https://github.com/standardnotes/server/issues/692)
# [1.25.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.24.2...@standardnotes/domain-core@1.25.0) (2023-08-09)
### Features
* **syncing-server:** notify shared vault users upon file uploads or removals ([#692](https://github.com/standardnotes/server/issues/692)) ([46867c1](https://github.com/standardnotes/server/commit/46867c1a4dd310c1971ff37e1bdf380c10e478fd))
## [1.24.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.24.1...@standardnotes/domain-core@1.24.2) (2023-08-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.24.2",
"version": "1.26.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -21,25 +21,36 @@ describe('RoleName', () => {
const plusUserRole = RoleName.create(RoleName.NAMES.PlusUser).getValue()
const coreUser = RoleName.create(RoleName.NAMES.CoreUser).getValue()
const internalTeamUser = RoleName.create(RoleName.NAMES.InternalTeamUser).getValue()
const transitionUser = RoleName.create(RoleName.NAMES.TransitionUser).getValue()
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(internalTeamUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(proUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(plusUserRole.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(plusUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(plusUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
expect(plusUserRole.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(plusUserRole.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(coreUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(coreUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
expect(transitionUser.hasMoreOrEqualPowerTo(internalTeamUser)).toBeFalsy()
expect(transitionUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(transitionUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
expect(transitionUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(transitionUser.hasMoreOrEqualPowerTo(transitionUser)).toBeTruthy()
})
})

View File

@@ -8,6 +8,7 @@ export class RoleName extends ValueObject<RoleNameProps> {
PlusUser: 'PLUS_USER',
ProUser: 'PRO_USER',
InternalTeamUser: 'INTERNAL_TEAM_USER',
TransitionUser: 'TRANSITION_USER',
}
get value(): string {
@@ -19,11 +20,19 @@ export class RoleName extends ValueObject<RoleNameProps> {
case RoleName.NAMES.InternalTeamUser:
return true
case RoleName.NAMES.ProUser:
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser].includes(roleName.value)
return [
RoleName.NAMES.CoreUser,
RoleName.NAMES.PlusUser,
RoleName.NAMES.ProUser,
RoleName.NAMES.TransitionUser,
].includes(roleName.value)
case RoleName.NAMES.PlusUser:
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser].includes(roleName.value)
return [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.TransitionUser].includes(
roleName.value,
)
case RoleName.NAMES.CoreUser:
return [RoleName.NAMES.CoreUser].includes(roleName.value)
case RoleName.NAMES.TransitionUser:
return [RoleName.NAMES.CoreUser, RoleName.NAMES.TransitionUser].includes(roleName.value)
/*istanbul ignore next*/
default:
throw new Error(`Invalid role name: ${this.value}`)

View File

@@ -3,32 +3,24 @@ import { RoleNameCollection } from './RoleNameCollection'
describe('RoleNameCollection', () => {
it('should create a value object', () => {
const role1 = RoleName.create(RoleName.NAMES.ProUser).getValue()
const valueOrError = RoleNameCollection.create([role1])
const valueOrError = RoleNameCollection.create([RoleName.NAMES.ProUser])
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual([role1])
expect(valueOrError.getValue().value[0].value).toEqual('PRO_USER')
})
it('should tell if collections are not equal', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
let roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
]).getValue()
let roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser]).getValue()
let valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
RoleName.NAMES.ProUser,
RoleName.NAMES.PlusUser,
RoleName.NAMES.CoreUser,
]).getValue()
valueOrError = RoleNameCollection.create(roles1)
@@ -36,42 +28,30 @@ describe('RoleNameCollection', () => {
})
it('should tell if collections are equal', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
const roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]).getValue()
const roles2 = RoleNameCollection.create([RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]).getValue()
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeTruthy()
})
it('should tell if collection includes element', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeTruthy()
})
it('should tell if collection does not includes element', () => {
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles1 = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.CoreUser).getValue())).toBeFalsy()
})
it('should tell if collection has a role with more or equal power to', () => {
let roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue()]
let roles = [RoleName.NAMES.CoreUser]
let valueOrError = RoleNameCollection.create(roles)
let roleNames = valueOrError.getValue()
@@ -83,7 +63,7 @@ describe('RoleNameCollection', () => {
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
roles = [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser]
valueOrError = RoleNameCollection.create(roles)
roleNames = valueOrError.getValue()
@@ -95,7 +75,7 @@ describe('RoleNameCollection', () => {
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
roles = [RoleName.create(RoleName.NAMES.ProUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
roles = [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser]
valueOrError = RoleNameCollection.create(roles)
roleNames = valueOrError.getValue()
@@ -109,4 +89,11 @@ describe('RoleNameCollection', () => {
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
})
it('should fail to create a collection if a role name is invalid', () => {
const valueOrError = RoleNameCollection.create(['invalid-role-name'])
expect(valueOrError.isFailed()).toBeTruthy()
expect(valueOrError.getError()).toEqual('Invalid role name: invalid-role-name')
})
})

View File

@@ -46,7 +46,16 @@ export class RoleNameCollection extends ValueObject<RoleNameCollectionProps> {
super(props)
}
static create(roleName: RoleName[]): Result<RoleNameCollection> {
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleName }))
static create(roleNameStrings: string[]): Result<RoleNameCollection> {
const roleNames: RoleName[] = []
for (const roleNameString of roleNameStrings) {
const roleNameOrError = RoleName.create(roleNameString)
if (roleNameOrError.isFailed()) {
return Result.fail<RoleNameCollection>(roleNameOrError.getError())
}
roleNames.push(roleNameOrError.getValue())
}
return Result.ok<RoleNameCollection>(new RoleNameCollection({ value: roleNames }))
}
}

View File

@@ -6,6 +6,8 @@ export class NotificationType extends ValueObject<NotificationTypeProps> {
static readonly TYPES = {
SharedVaultItemRemoved: 'shared_vault_item_removed',
RemovedFromSharedVault: 'removed_from_shared_vault',
SharedVaultFileUploaded: 'shared_vault_file_uploaded',
SharedVaultFileRemoved: 'shared_vault_file_removed',
}
get value(): string {

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.12.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.12...@standardnotes/domain-events-infra@1.12.13) (2023-08-23)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.11...@standardnotes/domain-events-infra@1.12.12) (2023-08-22)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.12.11](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.12.10...@standardnotes/domain-events-infra@1.12.11) (2023-08-08)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.12.11",
"version": "1.12.13",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.116.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.1...@standardnotes/domain-events@2.116.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
## [2.115.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.115.0...@standardnotes/domain-events@2.115.1) (2023-08-22)
**Note:** Version bump only for package @standardnotes/domain-events
# [2.115.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.114.0...@standardnotes/domain-events@2.115.0) (2023-08-08)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.115.0",
"version": "2.116.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload'
export interface SharedVaultFileMovedEvent extends DomainEventInterface {
type: 'SHARED_VAULT_FILE_MOVED'
payload: SharedVaultFileMovedEventPayload
}

View File

@@ -0,0 +1,14 @@
export interface SharedVaultFileMovedEventPayload {
fileByteSize: number
fileName: string
from: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
}

View File

@@ -64,6 +64,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
export * from './Event/SharedVaultFileMovedEvent'
export * from './Event/SharedVaultFileMovedEventPayload'
export * from './Event/SharedVaultFileRemovedEvent'
export * from './Event/SharedVaultFileRemovedEventPayload'
export * from './Event/SharedVaultFileUploadedEvent'

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.11.22](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.21...@standardnotes/event-store@1.11.22) (2023-08-23)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.21](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.20...@standardnotes/event-store@1.11.21) (2023-08-22)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.20](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.19...@standardnotes/event-store@1.11.20) (2023-08-18)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.19](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.18...@standardnotes/event-store@1.11.19) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.18](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.17...@standardnotes/event-store@1.11.18) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.16...@standardnotes/event-store@1.11.17) (2023-08-09)
**Note:** Version bump only for package @standardnotes/event-store
## [1.11.16](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.11.15...@standardnotes/event-store@1.11.16) (2023-08-08)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.11.16",
"version": "1.11.22",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

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.
# [1.22.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.21.0...@standardnotes/files-server@1.22.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/files/issues/705)) ([205a1ed](https://github.com/standardnotes/files/commit/205a1ed637b626be13fc656276508f3c7791024f))
# [1.21.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.4...@standardnotes/files-server@1.21.0) (2023-08-22)
### Features
* consider shared vault owner quota when uploading files to shared vault ([#704](https://github.com/standardnotes/files/issues/704)) ([34085ac](https://github.com/standardnotes/files/commit/34085ac6fb7e61d471bd3b4ae8e72112df25c3ee))
## [1.20.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.3...@standardnotes/files-server@1.20.4) (2023-08-18)
**Note:** Version bump only for package @standardnotes/files-server
## [1.20.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.2...@standardnotes/files-server@1.20.3) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.20.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.1...@standardnotes/files-server@1.20.2) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
## [1.20.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.20.0...@standardnotes/files-server@1.20.1) (2023-08-09)
**Note:** Version bump only for package @standardnotes/files-server
# [1.20.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.19.18...@standardnotes/files-server@1.20.0) (2023-08-08)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.20.0",
"version": "1.22.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -99,7 +99,9 @@ export class ContainerConfigLoader {
container
.bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
.toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
container
.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory)
.toConstantValue(new DomainEventFactory(container.get<TimerInterface>(TYPES.Files_Timer)))
if (isConfiguredForInMemoryCache) {
container
@@ -214,9 +216,26 @@ export class ContainerConfigLoader {
container.get(TYPES.Files_DomainEventFactory),
),
)
container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
container
.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata)
.toConstantValue(
new GetFileMetadata(
container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
container.get<winston.Logger>(TYPES.Files_Logger),
),
)
container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
container.bind<MoveFile>(TYPES.Files_MoveFile).to(MoveFile)
container
.bind<MoveFile>(TYPES.Files_MoveFile)
.toConstantValue(
new MoveFile(
container.get<GetFileMetadata>(TYPES.Files_GetFileMetadata),
container.get<FileMoverInterface>(TYPES.Files_FileMover),
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
container.get<winston.Logger>(TYPES.Files_Logger),
),
)
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
// middleware

View File

@@ -4,16 +4,14 @@ import {
DomainEventService,
SharedVaultFileUploadedEvent,
SharedVaultFileRemovedEvent,
SharedVaultFileMovedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
@injectable()
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
constructor(private timer: TimerInterface) {}
createFileRemovedEvent(payload: {
userUuid: string
@@ -56,6 +54,34 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createSharedVaultFileMovedEvent(payload: {
fileByteSize: number
fileName: string
from: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
}): SharedVaultFileMovedEvent {
return {
type: 'SHARED_VAULT_FILE_MOVED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: payload.from.sharedVaultUuid ?? payload.from.ownerUuid,
userIdentifierType: payload.from.sharedVaultUuid ? 'shared-vault-uuid' : 'uuid',
},
origin: DomainEventService.Files,
},
payload,
}
}
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string

View File

@@ -3,6 +3,7 @@ import {
FileRemovedEvent,
SharedVaultFileRemovedEvent,
SharedVaultFileUploadedEvent,
SharedVaultFileMovedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
@@ -19,6 +20,20 @@ export interface DomainEventFactoryInterface {
fileByteSize: number
regularSubscriptionUuid: string
}): FileRemovedEvent
createSharedVaultFileMovedEvent(payload: {
fileByteSize: number
fileName: string
from: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
filePath: string
}
}): SharedVaultFileMovedEvent
createSharedVaultFileUploadedEvent(payload: {
sharedVaultUuid: string
vaultOwnerUuid: string

View File

@@ -1,4 +1,3 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
@@ -19,10 +18,8 @@ describe('GetFileMetadata', () => {
})
it('should return the file metadata', async () => {
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
success: true,
size: 123,
})
const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
expect(result.getValue()).toEqual(123)
})
it('should not return the file metadata if it fails', async () => {
@@ -30,9 +27,8 @@ describe('GetFileMetadata', () => {
throw new Error('ooops')
})
expect(await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })).toEqual({
success: false,
message: 'Could not get file metadata.',
})
const result = await createUseCase().execute({ resourceRemoteIdentifier: '1-2-3', ownerUuid: '2-3-4' })
expect(result.isFailed()).toBe(true)
})
})

View File

@@ -1,32 +1,20 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { GetFileMetadataDTO } from './GetFileMetadataDTO'
import { GetFileMetadataResponse } from './GetFileMetadataResponse'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
@injectable()
export class GetFileMetadata implements UseCaseInterface {
constructor(
@inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface,
@inject(TYPES.Files_Logger) private logger: Logger,
) {}
export class GetFileMetadata implements UseCaseInterface<number> {
constructor(private fileDownloader: FileDownloaderInterface, private logger: Logger) {}
async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {
async execute(dto: GetFileMetadataDTO): Promise<Result<number>> {
try {
const size = await this.fileDownloader.getFileSize(`${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
return {
success: true,
size,
}
return Result.ok(size)
} catch (error) {
this.logger.error(`Could not get file metadata for resource: ${dto.ownerUuid}/${dto.resourceRemoteIdentifier}`)
return {
success: false,
message: 'Could not get file metadata.',
}
return Result.fail('Could not get file metadata')
}
}
}

View File

@@ -1,9 +0,0 @@
export type GetFileMetadataResponse =
| {
success: true
size: number
}
| {
success: false
message: string
}

View File

@@ -1,18 +1,27 @@
import 'reflect-metadata'
import { DomainEventPublisherInterface, SharedVaultFileMovedEvent } from '@standardnotes/domain-events'
import { Result } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { MoveFile } from './MoveFile'
import { FileMoverInterface } from '../../Services/FileMoverInterface'
import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
describe('MoveFile', () => {
let fileMover: FileMoverInterface
let getFileMetadataUseCase: GetFileMetadata
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let logger: Logger
const createUseCase = () => new MoveFile(fileMover, logger)
const createUseCase = () =>
new MoveFile(getFileMetadataUseCase, fileMover, domainEventPublisher, domainEventFactory, logger)
beforeEach(() => {
getFileMetadataUseCase = {} as jest.Mocked<GetFileMetadata>
getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.ok(1234))
fileMover = {} as jest.Mocked<FileMoverInterface>
fileMover.moveFile = jest.fn().mockReturnValue(413)
@@ -20,17 +29,34 @@ describe('MoveFile', () => {
logger.debug = jest.fn()
logger.error = jest.fn()
logger.warn = jest.fn()
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createSharedVaultFileMovedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<SharedVaultFileMovedEvent>)
})
it('should move a file', async () => {
await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
fromUuid: '1-2-3',
toUuid: '4-5-6',
moveType: 'shared-vault-to-user',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(fileMover.moveFile).toHaveBeenCalledWith('1-2-3/2-3-4', '4-5-6/2-3-4')
expect(fileMover.moveFile).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000/2-3-4',
'00000000-0000-0000-0000-000000000001/2-3-4',
)
})
it('should indicate an error if moving fails', async () => {
@@ -40,11 +66,174 @@ describe('MoveFile', () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
fromUuid: '1-2-3',
toUuid: '4-5-6',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the from shared vault uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: 'invalid',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the to shared vault uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: 'invalid',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the from owner uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: 'invalid',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the to owner uuid is invalid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: 'invalid',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should return an error if the file metadata cannot be retrieved', async () => {
getFileMetadataUseCase.execute = jest.fn().mockReturnValue(Result.fail('oops'))
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000001',
},
moveType: 'shared-vault-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
it('should move file from user to shared vault', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000002',
},
moveType: 'user-to-shared-vault',
})
expect(fileMover.moveFile).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000000/2-3-4',
'00000000-0000-0000-0000-000000000001/2-3-4',
)
expect(result.isFailed()).toEqual(false)
})
it('should move file from shared vault to user', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
sharedVaultUuid: '00000000-0000-0000-0000-000000000001',
ownerUuid: '00000000-0000-0000-0000-000000000002',
},
to: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
moveType: 'shared-vault-to-user',
})
expect(fileMover.moveFile).toHaveBeenCalledWith(
'00000000-0000-0000-0000-000000000001/2-3-4',
'00000000-0000-0000-0000-000000000000/2-3-4',
)
expect(result.isFailed()).toEqual(false)
})
it('should fail if moving from shared vault to user without shared vault uuid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
moveType: 'shared-vault-to-user',
})
expect(result.isFailed()).toEqual(true)
})
it('should fail if moving from user to shared vault without shared vault uuid', async () => {
const result = await createUseCase().execute({
resourceRemoteIdentifier: '2-3-4',
from: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
to: {
ownerUuid: '00000000-0000-0000-0000-000000000000',
},
moveType: 'user-to-shared-vault',
})
expect(result.isFailed()).toEqual(true)
})
})

View File

@@ -1,27 +1,97 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import TYPES from '../../../Bootstrap/Types'
import { FileMoverInterface } from '../../Services/FileMoverInterface'
import { MoveFileDTO } from './MoveFileDTO'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { GetFileMetadata } from '../GetFileMetadata/GetFileMetadata'
@injectable()
export class MoveFile implements UseCaseInterface<boolean> {
constructor(
@inject(TYPES.Files_FileMover) private fileMover: FileMoverInterface,
@inject(TYPES.Files_Logger) private logger: Logger,
private getFileMetadataUseCase: GetFileMetadata,
private fileMover: FileMoverInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private logger: Logger,
) {}
async execute(dto: MoveFileDTO): Promise<Result<boolean>> {
try {
const srcPath = `${dto.fromUuid}/${dto.resourceRemoteIdentifier}`
const destPath = `${dto.toUuid}/${dto.resourceRemoteIdentifier}`
let fromSharedVaultUuid: Uuid | undefined = undefined
if (dto.from.sharedVaultUuid !== undefined) {
const fromSharedVaultUuidOrError = Uuid.create(dto.from.sharedVaultUuid)
if (fromSharedVaultUuidOrError.isFailed()) {
return Result.fail(fromSharedVaultUuidOrError.getError())
}
fromSharedVaultUuid = fromSharedVaultUuidOrError.getValue()
}
let toSharedVaultUuid: Uuid | undefined = undefined
if (dto.to.sharedVaultUuid !== undefined) {
const toSharedVaultUuidOrError = Uuid.create(dto.to.sharedVaultUuid)
if (toSharedVaultUuidOrError.isFailed()) {
return Result.fail(toSharedVaultUuidOrError.getError())
}
toSharedVaultUuid = toSharedVaultUuidOrError.getValue()
}
const fromOwnerUuidOrError = Uuid.create(dto.from.ownerUuid)
if (fromOwnerUuidOrError.isFailed()) {
return Result.fail(fromOwnerUuidOrError.getError())
}
const fromOwnerUuid = fromOwnerUuidOrError.getValue()
const toOwnerUuidOrError = Uuid.create(dto.to.ownerUuid)
if (toOwnerUuidOrError.isFailed()) {
return Result.fail(toOwnerUuidOrError.getError())
}
const toOwnerUuid = toOwnerUuidOrError.getValue()
if (['shared-vault-to-shared-vault', 'shared-vault-to-user'].includes(dto.moveType) && !fromSharedVaultUuid) {
return Result.fail('Source shared vault UUID is required')
}
if (['user-to-shared-vault', 'shared-vault-to-shared-vault'].includes(dto.moveType) && !toSharedVaultUuid) {
return Result.fail('Target shared vault UUID is required')
}
const fromUuid = dto.moveType === 'user-to-shared-vault' ? fromOwnerUuid.value : fromSharedVaultUuid?.value
const toUuid = dto.moveType === 'shared-vault-to-user' ? toOwnerUuid.value : toSharedVaultUuid?.value
const srcPath = `${fromUuid}/${dto.resourceRemoteIdentifier}`
const destPath = `${toUuid}/${dto.resourceRemoteIdentifier}`
this.logger.debug(`Moving file from ${srcPath} to ${destPath}`)
const metadataResultOrError = await this.getFileMetadataUseCase.execute({
resourceRemoteIdentifier: dto.resourceRemoteIdentifier,
ownerUuid: fromUuid as string,
})
if (metadataResultOrError.isFailed()) {
return Result.fail(metadataResultOrError.getError())
}
const fileSize = metadataResultOrError.getValue()
await this.fileMover.moveFile(srcPath, destPath)
await this.domainEventPublisher.publish(
this.domainEventFactory.createSharedVaultFileMovedEvent({
fileByteSize: fileSize,
fileName: dto.resourceRemoteIdentifier,
from: {
sharedVaultUuid: fromSharedVaultUuid?.value,
ownerUuid: fromOwnerUuid.value,
filePath: srcPath,
},
to: {
sharedVaultUuid: toSharedVaultUuid?.value,
ownerUuid: toOwnerUuid.value,
filePath: destPath,
},
}),
)
return Result.ok()
} catch (error) {
this.logger.error(`Could not move resource: ${dto.resourceRemoteIdentifier} - ${(error as Error).message}`)

View File

@@ -2,7 +2,13 @@ import { SharedVaultMoveType } from '@standardnotes/security'
export interface MoveFileDTO {
moveType: SharedVaultMoveType
fromUuid: string
toUuid: string
from: {
sharedVaultUuid?: string
ownerUuid: string
}
to: {
sharedVaultUuid?: string
ownerUuid: string
}
resourceRemoteIdentifier: string
}

View File

@@ -61,7 +61,7 @@ describe('AnnotatedFilesController', () => {
finishUploadSession.execute = jest.fn().mockReturnValue(Result.ok())
getFileMetadata = {} as jest.Mocked<GetFileMetadata>
getFileMetadata.execute = jest.fn().mockReturnValue({ success: true, size: 555_555 })
getFileMetadata.execute = jest.fn().mockReturnValue(Result.ok(555_555))
removeFile = {} as jest.Mocked<RemoveFile>
removeFile.execute = jest.fn().mockReturnValue(Result.ok())
@@ -183,7 +183,7 @@ describe('AnnotatedFilesController', () => {
request.headers['range'] = 'bytes=0-'
getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
getFileMetadata.execute = jest.fn().mockReturnValue(Result.fail('error'))
const httpResponse = await createController().download(request, response)

View File

@@ -146,20 +146,21 @@ export class AnnotatedFilesController extends BaseHttpController {
chunkSize = this.maxChunkBytes
}
const fileMetadata = await this.getFileMetadata.execute({
const fileMetadataOrError = await this.getFileMetadata.execute({
ownerUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
})
if (!fileMetadata.success) {
return this.badRequest(fileMetadata.message)
if (fileMetadataOrError.isFailed()) {
return this.badRequest(fileMetadataOrError.getError())
}
const fileSize = fileMetadataOrError.getValue()
const startRange = Number(range.replace(/\D/g, ''))
const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
const headers = {
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': endRange - startRange + 1,
'Content-Type': 'application/octet-stream',

View File

@@ -47,8 +47,8 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
const result = await this.moveFile.execute({
moveType: moveOperation.type,
fromUuid: moveOperation.fromUuid,
toUuid: moveOperation.toUuid,
from: moveOperation.from,
to: moveOperation.to,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
@@ -121,6 +121,10 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
return this.badRequest('Not permitted for this operation')
}
if (locals.uploadBytesLimit === undefined) {
return this.badRequest('Missing upload bytes limit')
}
const result = await this.finishUploadSession.execute({
userUuid: locals.vaultOwnerUuid,
sharedVaultUuid: locals.sharedVaultUuid,
@@ -183,20 +187,21 @@ export class AnnotatedSharedVaultFilesController extends BaseHttpController {
chunkSize = this.maxChunkBytes
}
const fileMetadata = await this.getFileMetadata.execute({
const fileMetadataOrError = await this.getFileMetadata.execute({
ownerUuid: locals.sharedVaultUuid,
resourceRemoteIdentifier: locals.remoteIdentifier,
})
if (!fileMetadata.success) {
return this.badRequest(fileMetadata.message)
if (fileMetadataOrError.isFailed()) {
return this.badRequest(fileMetadataOrError.getError())
}
const fileSize = fileMetadataOrError.getValue()
const startRange = Number(range.replace(/\D/g, ''))
const endRange = Math.min(startRange + chunkSize - 1, fileMetadata.size - 1)
const endRange = Math.min(startRange + chunkSize - 1, fileSize - 1)
const headers = {
'Content-Range': `bytes ${startRange}-${endRange}/${fileMetadata.size}`,
'Content-Range': `bytes ${startRange}-${endRange}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': endRange - startRange + 1,
'Content-Type': 'application/octet-stream',

View File

@@ -1,4 +1,4 @@
import { SharedVaultValetTokenData, TokenDecoderInterface } from '@standardnotes/security'
import { SharedVaultValetTokenData, TokenDecoderInterface, ValetTokenOperation } from '@standardnotes/security'
import { Uuid } from '@standardnotes/domain-core'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
@@ -61,6 +61,17 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
return
}
if (this.userHasNoSpaceToUpload(valetTokenData)) {
response.status(403).send({
error: {
tag: 'no-space',
message: 'The file you are trying to upload is too big. Please ask the vault owner to upgrade subscription',
},
})
return
}
const whitelistedData: SharedVaultValetTokenData = {
sharedVaultUuid: valetTokenData.sharedVaultUuid,
vaultOwnerUuid: valetTokenData.vaultOwnerUuid,
@@ -79,4 +90,32 @@ export class SharedVaultValetTokenAuthMiddleware extends BaseMiddleware {
return next(error)
}
}
private userHasNoSpaceToUpload(valetTokenData: SharedVaultValetTokenData) {
if (![ValetTokenOperation.Write, ValetTokenOperation.Move].includes(valetTokenData.permittedOperation)) {
return false
}
if (valetTokenData.uploadBytesLimit === -1) {
return false
}
const isMovingToNonSharedVault =
valetTokenData.permittedOperation === ValetTokenOperation.Move &&
valetTokenData.moveOperation?.type === 'shared-vault-to-user'
if (isMovingToNonSharedVault) {
return false
}
if (valetTokenData.uploadBytesLimit === undefined) {
return true
}
const remainingUploadSpace = valetTokenData.uploadBytesLimit - valetTokenData.uploadBytesUsed
const consideredUploadSize = valetTokenData.unencryptedFileSize as number
return remainingUploadSpace - consideredUploadSize <= 0
}
}

View File

@@ -9,3 +9,12 @@ PSEUDO_KEY_PARAMS_KEY=
VALET_TOKEN_SECRET=
FILES_SERVER_URL=
SECONDARY_DB_ENABLED=false
MONGO_HOST=localhost
MONGO_PORT=27017
MONGO_USERNAME=standardnotes
MONGO_PASSWORD=standardnotes
MONGO_DATABASE=standardnotes
TRANSITION_MODE_ENABLED=false

View File

@@ -3,6 +3,96 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.15.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.2...@standardnotes/home-server@1.15.0) (2023-08-23)
### Features
* add handling file moving and updating storage quota ([#705](https://github.com/standardnotes/server/issues/705)) ([205a1ed](https://github.com/standardnotes/server/commit/205a1ed637b626be13fc656276508f3c7791024f))
## [1.14.2](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.1...@standardnotes/home-server@1.14.2) (2023-08-22)
**Note:** Version bump only for package @standardnotes/home-server
## [1.14.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.14.0...@standardnotes/home-server@1.14.1) (2023-08-21)
**Note:** Version bump only for package @standardnotes/home-server
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.51...@standardnotes/home-server@1.14.0) (2023-08-18)
### Features
* add mechanism for determining if a user should use the primary or secondary items database ([#700](https://github.com/standardnotes/server/issues/700)) ([302b624](https://github.com/standardnotes/server/commit/302b624504f4c87fd7c3ddfee77cbdc14a61018b))
## [1.13.51](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.50...@standardnotes/home-server@1.13.51) (2023-08-17)
### Bug Fixes
* **home-server:** add default env values for secondary database ([916e989](https://github.com/standardnotes/server/commit/916e98936a276a3960d949c5b70803214c945686))
## [1.13.50](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.49...@standardnotes/home-server@1.13.50) (2023-08-16)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.49](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.48...@standardnotes/home-server@1.13.49) (2023-08-15)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.48](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.47...@standardnotes/home-server@1.13.48) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.47](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.46...@standardnotes/home-server@1.13.47) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.46](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.45...@standardnotes/home-server@1.13.46) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.45](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.44...@standardnotes/home-server@1.13.45) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.44](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.43...@standardnotes/home-server@1.13.44) (2023-08-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.43](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.42...@standardnotes/home-server@1.13.43) (2023-08-10)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.42](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.41...@standardnotes/home-server@1.13.42) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.41](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.40...@standardnotes/home-server@1.13.41) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.40](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.39...@standardnotes/home-server@1.13.40) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.39](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.38...@standardnotes/home-server@1.13.39) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.38](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.37...@standardnotes/home-server@1.13.38) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.37](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.36...@standardnotes/home-server@1.13.37) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.36](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.35...@standardnotes/home-server@1.13.36) (2023-08-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.35](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.34...@standardnotes/home-server@1.13.35) (2023-08-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.13.34](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.13.33...@standardnotes/home-server@1.13.34) (2023-08-08)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.13.34",
"version": "1.15.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -144,6 +144,7 @@ export class HomeServer implements HomeServerInterface {
void this.activatePremiumFeatures({
username: request.body.username,
subscriptionPlanName: request.body.subscriptionPlanName,
uploadBytesLimit: request.body.uploadBytesLimit,
endsAt: request.body.endsAt ? new Date(request.body.endsAt) : undefined,
}).then((result) => {
if (result.isFailed()) {
@@ -221,6 +222,7 @@ export class HomeServer implements HomeServerInterface {
async activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>> {
if (!this.isRunning() || !this.authService) {

View File

@@ -6,6 +6,7 @@ export interface HomeServerInterface {
activatePremiumFeatures(dto: {
username: string
subscriptionPlanName?: string
uploadBytesLimit?: number
endsAt?: Date
}): Promise<Result<string>>
stop(): Promise<Result<string>>

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.
## [1.26.10](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.9...@standardnotes/revisions-server@1.26.10) (2023-08-23)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.8...@standardnotes/revisions-server@1.26.9) (2023-08-22)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.7...@standardnotes/revisions-server@1.26.8) (2023-08-18)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.6...@standardnotes/revisions-server@1.26.7) (2023-08-11)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.5...@standardnotes/revisions-server@1.26.6) (2023-08-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.4...@standardnotes/revisions-server@1.26.5) (2023-08-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.3...@standardnotes/revisions-server@1.26.4) (2023-08-09)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.26.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.26.2...@standardnotes/revisions-server@1.26.3) (2023-08-08)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.26.3",
"version": "1.26.10",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

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