mirror of
https://github.com/standardnotes/server
synced 2026-04-20 11:02:33 -04:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b510284e01 | |||
| 205a1ed637 | |||
| 2073c735a5 | |||
| 34085ac6fb | |||
| 3d6559921b | |||
| 15a7f0e71a | |||
| 3e56243d6f | |||
| 032fcb938d | |||
| e98393452b | |||
| 302b624504 | |||
| e00d9d2ca0 | |||
| 9ab4601c8d | |||
| 19e43bdb1a | |||
| 49832e7944 | |||
| 916e98936a | |||
| 31d1eef7f7 | |||
| 2648d9a813 | |||
| b24b576209 | |||
| faee38bffd | |||
| 65f3503fe8 | |||
| 054023b791 | |||
| 383c3a68fa | |||
| 7d22b1c15c | |||
| c71e7cd926 | |||
| 83ad069c5d | |||
| 081108d9ba | |||
| 8f3df56a2b | |||
| d02124f4e5 | |||
| 09e351fedb | |||
| ad4b85b095 | |||
| 0bf7d8beae | |||
| 1ae7cca394 | |||
| bc1c7a8ae1 | |||
| c22c5e4584 | |||
| ac3646836c | |||
| 7a31ab75d6 | |||
| c49dc35ab5 | |||
| 06cedd11d8 | |||
| f496376fb3 | |||
| 091e2a57e8 | |||
| 0d40ef6796 | |||
| 1be33ba4c3 | |||
| aaeb311928 | |||
| a7a38c07ac | |||
| 56f49752b4 | |||
| 892d8b6fe2 | |||
| cec2005436 | |||
| 0eb86c0096 | |||
| b8e39d76c1 | |||
| 1c3ff526b7 | |||
| 373767248c | |||
| d7965b2748 | |||
| cbcd2ec87a | |||
| c74d37fc48 | |||
| 66f9352a06 | |||
| e5eef3aba0 | |||
| d261c81cd0 | |||
| 634e3bbb67 | |||
| f8c9e67063 | |||
| 18eddea6f8 |
+7
-1
@@ -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
|
||||
|
||||
@@ -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() }}
|
||||
|
||||
@@ -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.
BIN
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.
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
#########
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.25.10",
|
||||
"version": "2.25.16",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.70.2",
|
||||
"version": "1.71.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -27,16 +27,23 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
}
|
||||
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
const sharedVaultOwnerContextHeaderValue = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
const cacheKey = `${authHeaderValue}${
|
||||
sharedVaultOwnerContextHeaderValue ? `:${sharedVaultOwnerContextHeaderValue}` : ''
|
||||
}`
|
||||
|
||||
try {
|
||||
let crossServiceTokenFetchedFromCache = true
|
||||
let crossServiceToken = null
|
||||
if (this.crossServiceTokenCacheTTL) {
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
|
||||
crossServiceToken = await this.crossServiceTokenCache.get(cacheKey)
|
||||
}
|
||||
|
||||
if (crossServiceToken === null) {
|
||||
const authResponse = await this.serviceProxy.validateSession(authHeaderValue)
|
||||
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)
|
||||
|
||||
@@ -12,29 +12,29 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
let userAuthHeaders = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthHeaders = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeys = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${dto.userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeys = JSON.parse(userKeysJSON)
|
||||
}
|
||||
userAuthHeaders.push(dto.authorizationHeaderValue)
|
||||
userKeys.push(dto.key)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userKeys))
|
||||
this.crossServiceTokenTTLCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
this.crossServiceTokenCache.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
this.crossServiceTokenTTLCache.set(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
async get(key: string): Promise<string | null> {
|
||||
this.invalidateExpiredTokens()
|
||||
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${key}`)
|
||||
if (!cachedToken) {
|
||||
return null
|
||||
}
|
||||
@@ -43,15 +43,15 @@ export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInt
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
let userAuthorizationHeaderValues = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
|
||||
let userKeyValues = []
|
||||
const userKeysJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userKeysJSON) {
|
||||
userKeyValues = JSON.parse(userKeysJSON)
|
||||
}
|
||||
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${key}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
@@ -12,32 +12,32 @@ export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterf
|
||||
constructor(@inject(TYPES.ApiGateway_Redis) private redisClient: IORedis.Redis) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void> {
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
|
||||
pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.key)
|
||||
pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
|
||||
|
||||
pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
|
||||
pipeline.set(`${this.PREFIX}:${dto.key}`, dto.encodedCrossServiceToken)
|
||||
pipeline.expireat(`${this.PREFIX}:${dto.key}`, dto.expiresAtInSeconds)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
async get(key: string): Promise<string | null> {
|
||||
return this.redisClient.get(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
const userKeyValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
for (const key of userKeyValues) {
|
||||
pipeline.del(`${this.PREFIX}:${key}`)
|
||||
}
|
||||
pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface CrossServiceTokenCacheInterface {
|
||||
set(dto: {
|
||||
authorizationHeaderValue: string
|
||||
key: string
|
||||
encodedCrossServiceToken: string
|
||||
expiresAtInSeconds: number
|
||||
userUuid: string
|
||||
}): Promise<void>
|
||||
get(authorizationHeaderValue: string): Promise<string | null>
|
||||
get(key: string): Promise<string | null>
|
||||
invalidate(userUuid: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -24,14 +24,16 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@inject(TYPES.ApiGateway_Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authorizationHeaderValue,
|
||||
Authorization: headers.authorization,
|
||||
Accept: 'application/json',
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface ServiceProxyInterface {
|
||||
endpointOrMethodIdentifier: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void>
|
||||
validateSession(authorizationHeaderValue: string): Promise<{
|
||||
validateSession(headers: { authorization: string; sharedVaultOwnerContext?: string }): Promise<{
|
||||
status: number
|
||||
data: unknown
|
||||
headers: {
|
||||
|
||||
@@ -6,9 +6,10 @@ import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
|
||||
export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
constructor(private serviceContainer: ServiceContainerInterface, private filesServerUrl: string) {}
|
||||
|
||||
async validateSession(
|
||||
authorizationHeaderValue: string,
|
||||
): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
async validateSession(headers: {
|
||||
authorization: string
|
||||
sharedVaultOwnerContext?: string
|
||||
}): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
|
||||
const authService = this.serviceContainer.get(ServiceIdentifier.create(ServiceIdentifier.NAMES.Auth).getValue())
|
||||
if (!authService) {
|
||||
throw new Error('Auth service not found')
|
||||
@@ -17,7 +18,8 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
const serviceResponse = (await authService.handleRequest(
|
||||
{
|
||||
headers: {
|
||||
authorization: authorizationHeaderValue,
|
||||
authorization: headers.authorization,
|
||||
'x-shared-vault-owner-context': headers.sharedVaultOwnerContext,
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348191367 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class AddTransitionRole1692348280258 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'INSERT INTO `roles` (uuid, name, version) VALUES ("e7381dc5-3d67-49e9-b7bd-f2407b2f726e", "TRANSITION_USER", 1)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.131.1",
|
||||
"version": "1.134.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface ActivatePremiumFeaturesDTO {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}
|
||||
|
||||
+79
-19
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+30
-10
@@ -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 } {
|
||||
|
||||
@@ -6,6 +6,7 @@ export type CreateCrossServiceTokenDTO = Either<
|
||||
{
|
||||
user: User
|
||||
session?: Session
|
||||
sharedVaultOwnerContext?: string
|
||||
},
|
||||
{
|
||||
userUuid: string
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export type CreateCrossServiceTokenResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -73,35 +73,30 @@ describe('GetSetting', () => {
|
||||
|
||||
describe('no subscription', () => {
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not find a setting if the setting name is invalid', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Invalid setting name: invalid',
|
||||
},
|
||||
})
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not get a setting for user if it does not exist', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.DropboxBackupFrequency,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive setting for user', async () => {
|
||||
@@ -112,21 +107,19 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not retrieve a subscription setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
|
||||
@@ -137,14 +130,13 @@ describe('GetSetting', () => {
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MfaSecret,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'bar' },
|
||||
})
|
||||
@@ -159,10 +151,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -171,14 +165,11 @@ describe('GetSetting', () => {
|
||||
it('should not get a suscription setting for user if it does not exist', async () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
|
||||
},
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not retrieve a sensitive subscription setting for user', async () => {
|
||||
@@ -188,10 +179,12 @@ describe('GetSetting', () => {
|
||||
.fn()
|
||||
.mockReturnValue(subscriptionSetting)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
sensitive: true,
|
||||
})
|
||||
})
|
||||
@@ -205,10 +198,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.MuteSignInEmails,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
@@ -221,10 +216,12 @@ describe('GetSetting', () => {
|
||||
})
|
||||
|
||||
it('should find a regular subscription only setting for user', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
settingName: SettingName.NAMES.FileUploadBytesLimit,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(result.getValue()).toEqual({
|
||||
userUuid: '1-2-3',
|
||||
setting: { foo: 'sub-bar' },
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingProjector } from '../../../Projection/SettingProjector'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
@@ -14,7 +14,7 @@ import { GetSettingResponse } from './GetSettingResponse'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
|
||||
@injectable()
|
||||
export class GetSetting implements UseCaseInterface {
|
||||
export class GetSetting implements UseCaseInterface<GetSettingResponse> {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SettingProjector) private settingProjector: SettingProjector,
|
||||
@inject(TYPES.Auth_SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
|
||||
@@ -24,15 +24,10 @@ export class GetSetting implements UseCaseInterface {
|
||||
@inject(TYPES.Auth_UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
|
||||
async execute(dto: GetSettingDto): Promise<Result<GetSettingResponse>> {
|
||||
const settingNameOrError = SettingName.create(dto.settingName)
|
||||
if (settingNameOrError.isFailed()) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: settingNameOrError.getError(),
|
||||
},
|
||||
}
|
||||
return Result.fail(settingNameOrError.getError())
|
||||
}
|
||||
const settingName = settingNameOrError.getValue()
|
||||
|
||||
@@ -47,12 +42,7 @@ export class GetSetting implements UseCaseInterface {
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'No subscription found.',
|
||||
},
|
||||
}
|
||||
return Result.fail('No subscription found.')
|
||||
}
|
||||
|
||||
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
|
||||
@@ -62,28 +52,21 @@ export class GetSetting implements UseCaseInterface {
|
||||
})
|
||||
|
||||
if (subscriptionSetting === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
|
||||
},
|
||||
}
|
||||
return Result.fail(`Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`)
|
||||
}
|
||||
|
||||
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
sensitive: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
userUuid: dto.userUuid,
|
||||
setting: simpleSubscriptionSetting,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setting = await this.settingService.findSettingWithDecryptedValue({
|
||||
@@ -92,27 +75,20 @@ export class GetSetting implements UseCaseInterface {
|
||||
})
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
|
||||
},
|
||||
}
|
||||
return Result.fail(`Setting ${settingName.value} for user ${dto.userUuid} not found!`)
|
||||
}
|
||||
|
||||
if (setting.sensitive && !dto.allowSensitiveRetrieval) {
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
sensitive: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleSetting = await this.settingProjector.projectSimple(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
return Result.ok({
|
||||
userUuid: dto.userUuid,
|
||||
setting: simpleSetting,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { SimpleSetting } from '../../Setting/SimpleSetting'
|
||||
|
||||
export type GetSettingResponse =
|
||||
| {
|
||||
success: true
|
||||
userUuid: string
|
||||
setting: SimpleSetting
|
||||
}
|
||||
| {
|
||||
success: true
|
||||
sensitive: true
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
error: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
export type GetSettingResponse = Either<
|
||||
{
|
||||
userUuid: string
|
||||
setting: SimpleSetting
|
||||
},
|
||||
{
|
||||
sensitive: true
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+6
-10
@@ -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(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import { results } from 'inversify-express-utils'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedInternalController', () => {
|
||||
let getUserFeatures: GetUserFeatures
|
||||
@@ -73,7 +74,7 @@ describe('AnnotatedInternalController', () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
request.params.settingName = 'foobar'
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -91,7 +92,7 @@ describe('AnnotatedInternalController', () => {
|
||||
request.params.userUuid = '1-2-3'
|
||||
request.params.settingName = 'foobar'
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
@@ -36,16 +36,26 @@ export class AnnotatedInternalController extends BaseHttpController {
|
||||
|
||||
@httpGet('/users/:userUuid/settings/:settingName')
|
||||
async getSetting(request: Request): Promise<results.JsonResult> {
|
||||
const result = await this.doGetSetting.execute({
|
||||
const resultOrError = await this.doGetSetting.execute({
|
||||
userUuid: request.params.userUuid,
|
||||
settingName: request.params.settingName,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossService
|
||||
import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
|
||||
import { Session } from '../../Domain/Session/Session'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSessionsController', () => {
|
||||
let getActiveSessionsForUser: GetActiveSessionsForUser
|
||||
@@ -45,7 +46,7 @@ describe('AnnotatedSessionsController', () => {
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
|
||||
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue(Result.ok('foobar'))
|
||||
|
||||
request = {
|
||||
params: {},
|
||||
|
||||
@@ -10,6 +10,7 @@ import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
|
||||
import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
|
||||
import { User } from '../../Domain/User/User'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
describe('AnnotatedSettingsController', () => {
|
||||
let deleteSetting: DeleteSetting
|
||||
@@ -85,7 +86,7 @@ describe('AnnotatedSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: true })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
@@ -119,7 +120,7 @@ describe('AnnotatedSettingsController', () => {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
|
||||
getSetting.execute = jest.fn().mockReturnValue({ success: false })
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().getSetting(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
+3
-2
@@ -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()
|
||||
|
||||
@@ -285,6 +285,10 @@ export class BaseAuthController extends BaseHttpController {
|
||||
authorizationHeader: <string>request.headers.authorization,
|
||||
})
|
||||
|
||||
if (result.headers?.has('x-invalidate-cache')) {
|
||||
response.setHeader('x-invalidate-cache', result.headers.get('x-invalidate-cache') as string)
|
||||
}
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,12 +45,25 @@ export class BaseSessionsController extends BaseHttpController {
|
||||
|
||||
const user = authenticateRequestResponse.user as User
|
||||
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
const sharedVaultOwnerContext = request.headers['x-shared-vault-owner-context'] as string | undefined
|
||||
|
||||
const resultOrError = await this.createCrossServiceToken.execute({
|
||||
user,
|
||||
session: authenticateRequestResponse.session,
|
||||
sharedVaultOwnerContext,
|
||||
})
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json({ authToken: result.token })
|
||||
return this.json({ authToken: resultOrError.getValue() })
|
||||
}
|
||||
|
||||
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
|
||||
@@ -58,13 +58,22 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const { userUuid, settingName } = request.params
|
||||
const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
|
||||
|
||||
if (result.success) {
|
||||
return this.json(result)
|
||||
const resultOrError = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
|
||||
if (resultOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: resultOrError.getError(),
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
return this.json(result, 400)
|
||||
return this.json({
|
||||
success: true,
|
||||
...resultOrError.getValue(),
|
||||
})
|
||||
}
|
||||
|
||||
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
|
||||
|
||||
+14
-4
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.50.0",
|
||||
"version": "1.50.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type ApplicationIdentifier = string
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.25.0",
|
||||
"version": "1.26.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.115.0",
|
||||
"version": "2.116.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { SharedVaultFileMovedEventPayload } from './SharedVaultFileMovedEventPayload'
|
||||
|
||||
export interface SharedVaultFileMovedEvent extends DomainEventInterface {
|
||||
type: 'SHARED_VAULT_FILE_MOVED'
|
||||
payload: SharedVaultFileMovedEventPayload
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.11.17",
|
||||
"version": "1.11.22",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -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.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.20.1",
|
||||
"version": "1.22.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export type GetFileMetadataResponse =
|
||||
| {
|
||||
success: true
|
||||
size: number
|
||||
}
|
||||
| {
|
||||
success: false
|
||||
message: string
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
+40
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,84 @@
|
||||
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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.13.37",
|
||||
"version": "1.15.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface HomeServerInterface {
|
||||
activatePremiumFeatures(dto: {
|
||||
username: string
|
||||
subscriptionPlanName?: string
|
||||
uploadBytesLimit?: number
|
||||
endsAt?: Date
|
||||
}): Promise<Result<string>>
|
||||
stop(): Promise<Result<string>>
|
||||
|
||||
@@ -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.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,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.26.4",
|
||||
"version": "1.26.10",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.26](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.25...@standardnotes/scheduler-server@1.20.26) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.25](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.24...@standardnotes/scheduler-server@1.20.25) (2023-08-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.24](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.23...@standardnotes/scheduler-server@1.20.24) (2023-08-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scheduler:** remove exit interview form link ([#702](https://github.com/standardnotes/server/issues/702)) ([3e56243](https://github.com/standardnotes/server/commit/3e56243d6f72f5ac86d4fb2191349ec3a589bc83))
|
||||
|
||||
## [1.20.23](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.22...@standardnotes/scheduler-server@1.20.23) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.22](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.21...@standardnotes/scheduler-server@1.20.22) (2023-08-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scheduler:** adjust email backups encouraging email schedule ([#695](https://github.com/standardnotes/server/issues/695)) ([091e2a5](https://github.com/standardnotes/server/commit/091e2a57e81bb335286807bba4b0fcfc04a086bb))
|
||||
|
||||
## [1.20.21](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.20...@standardnotes/scheduler-server@1.20.21) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.20](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.19...@standardnotes/scheduler-server@1.20.20) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.20.19](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.20.18...@standardnotes/scheduler-server@1.20.19) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user