mirror of
https://github.com/standardnotes/server
synced 2026-02-08 17:01:17 -05:00
Compare commits
23 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
093cc07c39 | ||
|
|
30f14820c6 | ||
|
|
c8ea2ab199 | ||
|
|
d56bbacc0b | ||
|
|
bb468a8b7e | ||
|
|
7e99f4b078 | ||
|
|
14ce6dd818 | ||
|
|
063a3e425d | ||
|
|
0900dc75ac | ||
|
|
aa8bd1f8dc | ||
|
|
c71f7ff8ad | ||
|
|
fe18420913 | ||
|
|
97124928df | ||
|
|
c108bfb12f | ||
|
|
5fe6ed1462 | ||
|
|
df5fcce769 | ||
|
|
8f57ece7b8 | ||
|
|
8a10d201c5 | ||
|
|
9d7e63a7a7 | ||
|
|
87c1ae2ac0 | ||
|
|
56c922e715 | ||
|
|
a29ac8e68f | ||
|
|
03f9c6039c |
2
.github/workflows/common-e2e.yml
vendored
2
.github/workflows/common-e2e.yml
vendored
@@ -22,6 +22,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
database: [ "mysql", "sqlite" ]
|
||||
cache: [ "redis", "memory" ]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -44,6 +45,7 @@ jobs:
|
||||
run: docker compose -f docker-compose.ci.yml up -d
|
||||
env:
|
||||
DB_TYPE: ${{ matrix.database }}
|
||||
CACHE_TYPE: ${{ matrix.cache }}
|
||||
|
||||
- name: Wait for server to start
|
||||
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
|
||||
|
||||
98
.pnp.cjs
generated
98
.pnp.cjs
generated
@@ -4500,6 +4500,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/responses", "npm:1.13.9"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/time", "workspace:packages/time"],\
|
||||
["@types/better-sqlite3", "npm:7.6.4"],\
|
||||
["@types/cors", "npm:2.8.12"],\
|
||||
["@types/dotenv", "npm:8.2.0"],\
|
||||
["@types/express", "npm:4.17.14"],\
|
||||
@@ -4507,6 +4508,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@types/jest", "npm:29.1.1"],\
|
||||
["@types/newrelic", "npm:9.13.0"],\
|
||||
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.48.2"],\
|
||||
["better-sqlite3", "npm:8.3.0"],\
|
||||
["cors", "npm:2.8.5"],\
|
||||
["dotenv", "npm:16.0.1"],\
|
||||
["eslint", "npm:8.32.0"],\
|
||||
@@ -4713,7 +4715,7 @@ const RAW_RUNTIME_STATE =
|
||||
["prettyjson", "npm:1.2.5"],\
|
||||
["reflect-metadata", "npm:0.1.13"],\
|
||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
|
||||
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.10"],\
|
||||
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.10"],\
|
||||
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
|
||||
["ua-parser-js", "npm:1.0.32"],\
|
||||
["uuid", "npm:9.0.0"],\
|
||||
@@ -14762,100 +14764,6 @@ const RAW_RUNTIME_STATE =
|
||||
["@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.29.3"],\
|
||||
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||
["dotenv", "npm:16.0.3"],\
|
||||
["glob", "npm:7.2.3"],\
|
||||
["hdb-pool", null],\
|
||||
["ioredis", null],\
|
||||
["js-yaml", "npm:4.1.0"],\
|
||||
["mkdirp", "npm:1.0.4"],\
|
||||
["mongodb", null],\
|
||||
["mssql", null],\
|
||||
["mysql2", "npm:3.0.1"],\
|
||||
["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", null],\
|
||||
["ts-node", null],\
|
||||
["tslib", "npm:2.4.0"],\
|
||||
["typeorm-aurora-data-api-driver", null],\
|
||||
["uuid", "npm:8.3.2"],\
|
||||
["xml2js", "npm:0.4.23"],\
|
||||
["yargs", "npm:17.5.1"]\
|
||||
],\
|
||||
"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:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.10", {\
|
||||
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-f86b034570/0/cache/typeorm-npm-0.3.10-4667857f33-749e1a6777.zip/node_modules/typeorm/",\
|
||||
"packageDependencies": [\
|
||||
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.10"],\
|
||||
["@google-cloud/spanner", null],\
|
||||
["@sap/hana-client", null],\
|
||||
["@sqltools/formatter", "npm:1.2.5"],\
|
||||
["@types/better-sqlite3", "npm:7.6.4"],\
|
||||
["@types/google-cloud__spanner", null],\
|
||||
["@types/hdb-pool", null],\
|
||||
|
||||
@@ -342,7 +342,7 @@ endif
|
||||
|
||||
quiet_cmd_regen_makefile = ACTION Regenerating $@
|
||||
cmd_regen_makefile = cd $(srcdir); /Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/karolsojko/Library/Caches/node-gyp/18.15.0" "-Dnode_gyp_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp" "-Dnode_lib_file=/Users/karolsojko/Library/Caches/node-gyp/18.15.0/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/better-sqlite3-npm-8.3.0-d1ef3f5776/node_modules/better-sqlite3" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/karolsojko/workspace/server/.yarn/unplugged/better-sqlite3-npm-8.3.0-d1ef3f5776/node_modules/better-sqlite3/build/config.gypi -I/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi -I/Users/karolsojko/Library/Caches/node-gyp/18.15.0/include/node/common.gypi "--toplevel-dir=." binding.gyp
|
||||
Makefile: $(srcdir)/deps/defines.gypi $(srcdir)/../../../../../../../Library/Caches/node-gyp/18.15.0/include/node/common.gypi $(srcdir)/deps/sqlite3.gyp $(srcdir)/binding.gyp $(srcdir)/../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/deps/common.gypi $(srcdir)/build/config.gypi
|
||||
Makefile: $(srcdir)/build/config.gypi $(srcdir)/binding.gyp $(srcdir)/../../../../../../../Library/Caches/node-gyp/18.15.0/include/node/common.gypi $(srcdir)/deps/defines.gypi $(srcdir)/../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/deps/common.gypi $(srcdir)/deps/sqlite3.gyp
|
||||
$(call do_cmd,regen_makefile)
|
||||
|
||||
# "all" is a concatenation of the "all" targets from all the included
|
||||
|
||||
@@ -60,6 +60,9 @@ fi
|
||||
if [ -z "$DB_TYPE" ]; then
|
||||
export DB_TYPE="mysql"
|
||||
fi
|
||||
if [ -z "$CACHE_TYPE" ]; then
|
||||
export CACHE_TYPE="redis"
|
||||
fi
|
||||
export DB_MIGRATIONS_PATH="dist/migrations/*.js"
|
||||
|
||||
#########
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [2.21.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.9...@standardnotes/analytics@2.21.10) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.21.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.21.8...@standardnotes/analytics@2.21.9) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.21.9",
|
||||
"version": "2.21.10",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
|
||||
CACHE_TYPE=redis
|
||||
REDIS_URL=redis://cache
|
||||
|
||||
# (Optional) Caching Cross Service Tokens
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.50.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.50.0...@standardnotes/api-gateway@1.50.1) (2023-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add env vars to control cache type for home server ([c8ea2ab](https://github.com/standardnotes/api-gateway/commit/c8ea2ab199bfd6d1836078fa26d578400a8099db))
|
||||
|
||||
# [1.50.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.13...@standardnotes/api-gateway@1.50.0) (2023-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **api-gateway:** add in memory cache for home server ([#582](https://github.com/standardnotes/api-gateway/issues/582)) ([0900dc7](https://github.com/standardnotes/api-gateway/commit/0900dc75ace12d263336c15d30d06a386b35ff20))
|
||||
|
||||
## [1.49.13](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.12...@standardnotes/api-gateway@1.49.13) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.49.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.49.11...@standardnotes/api-gateway@1.49.12) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.49.12",
|
||||
"version": "1.50.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionToken
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
|
||||
import { WebSocketAuthMiddleware } from '../Controller/WebSocketAuthMiddleware'
|
||||
import { InMemoryCrossServiceTokenCache } from '../Infra/InMemory/InMemoryCrossServiceTokenCache'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -26,6 +27,8 @@ export class ContainerConfigLoader {
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
|
||||
|
||||
const newrelicWinstonFormatter = newrelicFormatter(winston)
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
@@ -75,9 +78,16 @@ export class ContainerConfigLoader {
|
||||
|
||||
// Services
|
||||
container.bind<HttpServiceInterface>(TYPES.HTTPService).to(HttpService)
|
||||
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
container
|
||||
.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache)
|
||||
.toConstantValue(new InMemoryCrossServiceTokenCache(container.get(TYPES.Timer)))
|
||||
} else {
|
||||
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { CrossServiceTokenCacheInterface } from '../../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
|
||||
export class InMemoryCrossServiceTokenCache implements CrossServiceTokenCacheInterface {
|
||||
private readonly PREFIX = 'cst'
|
||||
private readonly USER_CST_PREFIX = 'user-cst'
|
||||
|
||||
private crossServiceTokenCache: Map<string, string> = new Map()
|
||||
private crossServiceTokenTTLCache: Map<string, number> = new Map()
|
||||
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async set(dto: {
|
||||
authorizationHeaderValue: 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)
|
||||
}
|
||||
userAuthHeaders.push(dto.authorizationHeaderValue)
|
||||
|
||||
this.crossServiceTokenCache.set(`${this.USER_CST_PREFIX}:${dto.userUuid}`, JSON.stringify(userAuthHeaders))
|
||||
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)
|
||||
}
|
||||
|
||||
async get(authorizationHeaderValue: string): Promise<string | null> {
|
||||
this.invalidateExpiredTokens()
|
||||
|
||||
const cachedToken = this.crossServiceTokenCache.get(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
if (!cachedToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
return cachedToken
|
||||
}
|
||||
|
||||
async invalidate(userUuid: string): Promise<void> {
|
||||
let userAuthorizationHeaderValues = []
|
||||
const userAuthHeadersJSON = this.crossServiceTokenCache.get(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
if (userAuthHeadersJSON) {
|
||||
userAuthorizationHeaderValues = JSON.parse(userAuthHeadersJSON)
|
||||
}
|
||||
|
||||
for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
|
||||
this.crossServiceTokenCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.PREFIX}:${authorizationHeaderValue}`)
|
||||
}
|
||||
this.crossServiceTokenCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
this.crossServiceTokenTTLCache.delete(`${this.USER_CST_PREFIX}:${userUuid}`)
|
||||
}
|
||||
|
||||
private invalidateExpiredTokens(): void {
|
||||
const nowInSeconds = this.timer.getTimestampInSeconds()
|
||||
for (const [key, expiresAtInSeconds] of this.crossServiceTokenTTLCache.entries()) {
|
||||
if (expiresAtInSeconds <= nowInSeconds) {
|
||||
this.crossServiceTokenCache.delete(key)
|
||||
this.crossServiceTokenTTLCache.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.103.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.103.0...@standardnotes/auth-server@1.103.1) (2023-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** invalidate cross service token cache upon shared subscription accepting ([#586](https://github.com/standardnotes/server/issues/586)) ([bb468a8](https://github.com/standardnotes/server/commit/bb468a8b7e9efbc9a95281d2438b19efe9c01a0c))
|
||||
|
||||
# [1.103.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.102.0...@standardnotes/auth-server@1.103.0) (2023-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
* extract cache entry model to domain-core ([#581](https://github.com/standardnotes/server/issues/581)) ([c71f7ff](https://github.com/standardnotes/server/commit/c71f7ff8ad4ffbd7151e8397b5816e383b178eb4))
|
||||
|
||||
# [1.102.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.101.0...@standardnotes/auth-server@1.102.0) (2023-05-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add sqlite driver repositories ([#580](https://github.com/standardnotes/server/issues/580)) ([9712492](https://github.com/standardnotes/server/commit/97124928df6298368408ee74cda71e2678d279dc))
|
||||
|
||||
# [1.101.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.100.0...@standardnotes/auth-server@1.101.0) (2023-05-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add sqlite offline subscription token repository for home server ([#579](https://github.com/standardnotes/server/issues/579)) ([5fe6ed1](https://github.com/standardnotes/server/commit/5fe6ed1462da3dcd1f40a10babf906fd522a3617))
|
||||
|
||||
# [1.100.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.99.0...@standardnotes/auth-server@1.100.0) (2023-05-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add sqlite ephemeral session repository for home server ([#578](https://github.com/standardnotes/server/issues/578)) ([8f57ece](https://github.com/standardnotes/server/commit/8f57ece7b88f7961eaf49144c4fdd72fbd07979b))
|
||||
|
||||
# [1.99.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.98.0...@standardnotes/auth-server@1.99.0) (2023-05-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add sqlite lock cache for home server ([#577](https://github.com/standardnotes/server/issues/577)) ([9d7e63a](https://github.com/standardnotes/server/commit/9d7e63a7a78adcb9817084e460a01189012bc403))
|
||||
|
||||
# [1.98.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.97.0...@standardnotes/auth-server@1.98.0) (2023-05-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add cache entries model ([#576](https://github.com/standardnotes/server/issues/576)) ([56c922e](https://github.com/standardnotes/server/commit/56c922e715167935885df2c4d93c5e1d685e0298))
|
||||
|
||||
# [1.97.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.96.0...@standardnotes/auth-server@1.97.0) (2023-04-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class cacheEntries1682926032072 implements MigrationInterface {
|
||||
name = 'cacheEntries1682926032072'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE `cache_entries` (`uuid` varchar(36) NOT NULL, `key` text NOT NULL, `value` text NOT NULL, `expires_at` datetime NULL, PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP TABLE `cache_entries`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class changeCacheTableName1683017908845 implements MigrationInterface {
|
||||
name = 'changeCacheTableName1683017908845'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('RENAME TABLE `cache_entries` TO `auth_cache_entries`')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('RENAME TABLE `auth_cache_entries` TO `cache_entries`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class cacheEntries1682925969528 implements MigrationInterface {
|
||||
name = 'cacheEntries1682925969528'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "cache_entries" ("uuid" varchar PRIMARY KEY NOT NULL, "key" text NOT NULL, "value" text NOT NULL, "expires_at" datetime)',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP TABLE "cache_entries"')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class changeCacheTableName1683017671034 implements MigrationInterface {
|
||||
name = 'changeCacheTableName1683017671034'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "cache_entries" RENAME TO "auth_cache_entries"')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "auth_cache_entries" RENAME TO "cache_entries"')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.97.0",
|
||||
"version": "1.103.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -171,6 +171,7 @@ import { SubscriptionSettingProjector } from '../Projection/SubscriptionSettingP
|
||||
import { SubscriptionSettingsAssociationService } from '../Domain/Setting/SubscriptionSettingsAssociationService'
|
||||
import { SubscriptionSettingsAssociationServiceInterface } from '../Domain/Setting/SubscriptionSettingsAssociationServiceInterface'
|
||||
import { PKCERepositoryInterface } from '../Domain/User/PKCERepositoryInterface'
|
||||
import { LockRepositoryInterface } from '../Domain/User/LockRepositoryInterface'
|
||||
import { RedisPKCERepository } from '../Infra/Redis/RedisPKCERepository'
|
||||
import { RoleRepositoryInterface } from '../Domain/Role/RoleRepositoryInterface'
|
||||
import { RevokedSessionRepositoryInterface } from '../Domain/Session/RevokedSessionRepositoryInterface'
|
||||
@@ -186,7 +187,7 @@ import { UserRequestsController } from '../Controller/UserRequestsController'
|
||||
import { EmailSubscriptionUnsubscribedEventHandler } from '../Domain/Handler/EmailSubscriptionUnsubscribedEventHandler'
|
||||
import { SessionTraceRepositoryInterface } from '../Domain/Session/SessionTraceRepositoryInterface'
|
||||
import { TypeORMSessionTraceRepository } from '../Infra/TypeORM/TypeORMSessionTraceRepository'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { CacheEntry, CacheEntryRepositoryInterface, MapperInterface } from '@standardnotes/domain-core'
|
||||
import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper'
|
||||
import { SessionTrace } from '../Domain/Session/SessionTrace'
|
||||
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
@@ -216,6 +217,15 @@ import { GenerateRecoveryCodes } from '../Domain/UseCase/GenerateRecoveryCodes/G
|
||||
import { SignInWithRecoveryCodes } from '../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||
import { GetUserKeyParamsRecovery } from '../Domain/UseCase/GetUserKeyParamsRecovery/GetUserKeyParamsRecovery'
|
||||
import { CleanupExpiredSessions } from '../Domain/UseCase/CleanupExpiredSessions/CleanupExpiredSessions'
|
||||
import { TypeORMCacheEntry } from '../Infra/TypeORM/TypeORMCacheEntry'
|
||||
import { TypeORMCacheEntryRepository } from '../Infra/TypeORM/TypeORMCacheEntryRepository'
|
||||
import { CacheEntryPersistenceMapper } from '../Mapping/CacheEntryPersistenceMapper'
|
||||
import { TypeORMLockRepository } from '../Infra/TypeORM/TypeORMLockRepository'
|
||||
import { EphemeralSessionRepositoryInterface } from '../Domain/Session/EphemeralSessionRepositoryInterface'
|
||||
import { TypeORMEphemeralSessionRepository } from '../Infra/TypeORM/TypeORMEphemeralSessionRepository'
|
||||
import { TypeORMOfflineSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMOfflineSubscriptionTokenRepository'
|
||||
import { TypeORMPKCERepository } from '../Infra/TypeORM/TypeORMPKCERepository'
|
||||
import { TypeORMSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMSubscriptionTokenRepository'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -229,6 +239,8 @@ export class ContainerConfigLoader {
|
||||
|
||||
await AppDataSource.initialize()
|
||||
|
||||
const isConfiguredForHomeServer = env.get('DB_TYPE') === 'sqlite'
|
||||
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
@@ -298,6 +310,9 @@ export class ContainerConfigLoader {
|
||||
TYPES.AuthenticatorChallengePersistenceMapper,
|
||||
)
|
||||
.toConstantValue(new AuthenticatorChallengePersistenceMapper())
|
||||
container
|
||||
.bind<MapperInterface<CacheEntry, TypeORMCacheEntry>>(TYPES.CacheEntryPersistenceMapper)
|
||||
.toConstantValue(new CacheEntryPersistenceMapper())
|
||||
|
||||
// ORM
|
||||
container
|
||||
@@ -335,6 +350,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<Repository<TypeORMAuthenticatorChallenge>>(TYPES.ORMAuthenticatorChallengeRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticatorChallenge))
|
||||
container
|
||||
.bind<Repository<TypeORMCacheEntry>>(TYPES.ORMCacheEntryRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMCacheEntry))
|
||||
|
||||
// Repositories
|
||||
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(TypeORMSessionRepository)
|
||||
@@ -356,20 +374,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<OfflineUserSubscriptionRepositoryInterface>(TYPES.OfflineUserSubscriptionRepository)
|
||||
.to(TypeORMOfflineUserSubscriptionRepository)
|
||||
container
|
||||
.bind<RedisEphemeralSessionRepository>(TYPES.EphemeralSessionRepository)
|
||||
.to(RedisEphemeralSessionRepository)
|
||||
container.bind<LockRepository>(TYPES.LockRepository).to(LockRepository)
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.SubscriptionTokenRepository)
|
||||
.to(RedisSubscriptionTokenRepository)
|
||||
container
|
||||
.bind<OfflineSubscriptionTokenRepositoryInterface>(TYPES.OfflineSubscriptionTokenRepository)
|
||||
.to(RedisOfflineSubscriptionTokenRepository)
|
||||
container
|
||||
.bind<SharedSubscriptionInvitationRepositoryInterface>(TYPES.SharedSubscriptionInvitationRepository)
|
||||
.to(TypeORMSharedSubscriptionInvitationRepository)
|
||||
container.bind<PKCERepositoryInterface>(TYPES.PKCERepository).to(RedisPKCERepository)
|
||||
container
|
||||
.bind<SessionTraceRepositoryInterface>(TYPES.SessionTraceRepository)
|
||||
.toConstantValue(
|
||||
@@ -394,6 +401,14 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.AuthenticatorChallengePersistenceMapper),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<CacheEntryRepositoryInterface>(TYPES.CacheEntryRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMCacheEntryRepository(
|
||||
container.get(TYPES.ORMCacheEntryRepository),
|
||||
container.get(TYPES.CacheEntryPersistenceMapper),
|
||||
),
|
||||
)
|
||||
|
||||
// Middleware
|
||||
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
|
||||
@@ -471,6 +486,62 @@ export class ContainerConfigLoader {
|
||||
.bind(TYPES.READONLY_USERS)
|
||||
.toConstantValue(env.get('READONLY_USERS', true) ? env.get('READONLY_USERS', true).split(',') : [])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
container
|
||||
.bind<LockRepositoryInterface>(TYPES.LockRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMLockRepository(
|
||||
container.get(TYPES.CacheEntryRepository),
|
||||
container.get(TYPES.Timer),
|
||||
container.get(TYPES.MAX_LOGIN_ATTEMPTS),
|
||||
container.get(TYPES.FAILED_LOGIN_LOCKOUT),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<EphemeralSessionRepositoryInterface>(TYPES.EphemeralSessionRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMEphemeralSessionRepository(
|
||||
container.get(TYPES.CacheEntryRepository),
|
||||
container.get(TYPES.EPHEMERAL_SESSION_AGE),
|
||||
container.get(TYPES.Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<OfflineSubscriptionTokenRepositoryInterface>(TYPES.OfflineSubscriptionTokenRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMOfflineSubscriptionTokenRepository(
|
||||
container.get(TYPES.CacheEntryRepository),
|
||||
container.get(TYPES.Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<PKCERepositoryInterface>(TYPES.PKCERepository)
|
||||
.toConstantValue(
|
||||
new TypeORMPKCERepository(
|
||||
container.get(TYPES.CacheEntryRepository),
|
||||
container.get(TYPES.Logger),
|
||||
container.get(TYPES.Timer),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.SubscriptionTokenRepository)
|
||||
.toConstantValue(
|
||||
new TypeORMSubscriptionTokenRepository(container.get(TYPES.CacheEntryRepository), container.get(TYPES.Timer)),
|
||||
)
|
||||
} else {
|
||||
container.bind<PKCERepositoryInterface>(TYPES.PKCERepository).to(RedisPKCERepository)
|
||||
container.bind<LockRepositoryInterface>(TYPES.LockRepository).to(LockRepository)
|
||||
container
|
||||
.bind<EphemeralSessionRepositoryInterface>(TYPES.EphemeralSessionRepository)
|
||||
.to(RedisEphemeralSessionRepository)
|
||||
container
|
||||
.bind<OfflineSubscriptionTokenRepositoryInterface>(TYPES.OfflineSubscriptionTokenRepository)
|
||||
.to(RedisOfflineSubscriptionTokenRepository)
|
||||
container
|
||||
.bind<SubscriptionTokenRepositoryInterface>(TYPES.SubscriptionTokenRepository)
|
||||
.to(RedisSubscriptionTokenRepository)
|
||||
}
|
||||
|
||||
// Services
|
||||
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
|
||||
container.bind<SessionService>(TYPES.SessionService).to(SessionService)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { UserSubscription } from '../Domain/Subscription/UserSubscription'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { TypeORMAuthenticator } from '../Infra/TypeORM/TypeORMAuthenticator'
|
||||
import { TypeORMAuthenticatorChallenge } from '../Infra/TypeORM/TypeORMAuthenticatorChallenge'
|
||||
import { TypeORMCacheEntry } from '../Infra/TypeORM/TypeORMCacheEntry'
|
||||
import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmergencyAccessInvitation'
|
||||
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
|
||||
import { Env } from './Env'
|
||||
@@ -68,6 +69,7 @@ const commonDataSourceOptions = {
|
||||
TypeORMAuthenticator,
|
||||
TypeORMAuthenticatorChallenge,
|
||||
TypeORMEmergencyAccessInvitation,
|
||||
TypeORMCacheEntry,
|
||||
],
|
||||
migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
|
||||
migrationsRun: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ const TYPES = {
|
||||
AuthenticatorChallengePersistenceMapper: Symbol.for('AuthenticatorChallengePersistenceMapper'),
|
||||
AuthenticatorPersistenceMapper: Symbol.for('AuthenticatorPersistenceMapper'),
|
||||
AuthenticatorHttpMapper: Symbol.for('AuthenticatorHttpMapper'),
|
||||
CacheEntryPersistenceMapper: Symbol.for('CacheEntryPersistenceMapper'),
|
||||
// Controller
|
||||
AuthController: Symbol.for('AuthController'),
|
||||
AuthenticatorsController: Symbol.for('AuthenticatorsController'),
|
||||
@@ -32,6 +33,7 @@ const TYPES = {
|
||||
SessionTraceRepository: Symbol.for('SessionTraceRepository'),
|
||||
AuthenticatorRepository: Symbol.for('AuthenticatorRepository'),
|
||||
AuthenticatorChallengeRepository: Symbol.for('AuthenticatorChallengeRepository'),
|
||||
CacheEntryRepository: Symbol.for('CacheEntryRepository'),
|
||||
// ORM
|
||||
ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'),
|
||||
ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'),
|
||||
@@ -46,6 +48,7 @@ const TYPES = {
|
||||
ORMSessionTraceRepository: Symbol.for('ORMSessionTraceRepository'),
|
||||
ORMAuthenticatorRepository: Symbol.for('ORMAuthenticatorRepository'),
|
||||
ORMAuthenticatorChallengeRepository: Symbol.for('ORMAuthenticatorChallengeRepository'),
|
||||
ORMCacheEntryRepository: Symbol.for('ORMCacheEntryRepository'),
|
||||
// Middleware
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
|
||||
|
||||
@@ -23,13 +23,14 @@ export class InversifyExpressSubscriptionInvitesController extends BaseHttpContr
|
||||
}
|
||||
|
||||
@httpPost('/:inviteUuid/accept', TYPES.ApiGatewayAuthMiddleware)
|
||||
async acceptInvite(request: Request): Promise<results.JsonResult> {
|
||||
const response = await this.subscriptionInvitesController.acceptInvite({
|
||||
async acceptInvite(request: Request, response: Response): Promise<void> {
|
||||
const result = await this.subscriptionInvitesController.acceptInvite({
|
||||
api: request.query.api as ApiVersion,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
})
|
||||
|
||||
return this.json(response.data, response.status)
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.status(result.status).send(result.data)
|
||||
}
|
||||
|
||||
@httpGet('/:inviteUuid/decline')
|
||||
|
||||
26
packages/auth/src/Infra/TypeORM/TypeORMCacheEntry.ts
Normal file
26
packages/auth/src/Infra/TypeORM/TypeORMCacheEntry.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'auth_cache_entries' })
|
||||
export class TypeORMCacheEntry {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
declare uuid: string
|
||||
|
||||
@Column({
|
||||
name: 'key',
|
||||
type: 'text',
|
||||
})
|
||||
declare key: string
|
||||
|
||||
@Column({
|
||||
name: 'value',
|
||||
type: 'text',
|
||||
})
|
||||
declare value: string
|
||||
|
||||
@Column({
|
||||
name: 'expires_at',
|
||||
type: 'datetime',
|
||||
nullable: true,
|
||||
})
|
||||
declare expiresAt: Date | null
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { CacheEntry, CacheEntryRepositoryInterface, MapperInterface } from '@standardnotes/domain-core'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import { TypeORMCacheEntry } from './TypeORMCacheEntry'
|
||||
|
||||
export class TypeORMCacheEntryRepository implements CacheEntryRepositoryInterface {
|
||||
constructor(
|
||||
private ormRepository: Repository<TypeORMCacheEntry>,
|
||||
private mapper: MapperInterface<CacheEntry, TypeORMCacheEntry>,
|
||||
) {}
|
||||
|
||||
async save(cacheEntry: CacheEntry): Promise<void> {
|
||||
const persistence = this.mapper.toProjection(cacheEntry)
|
||||
|
||||
await this.ormRepository.save(persistence)
|
||||
}
|
||||
|
||||
async findUnexpiredOneByKey(key: string): Promise<CacheEntry | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('cache')
|
||||
.where('cache.key = :key', {
|
||||
key,
|
||||
})
|
||||
.andWhere('cache.expires_at > :now', {
|
||||
now: new Date(),
|
||||
})
|
||||
.getOne()
|
||||
|
||||
if (persistence === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.mapper.toDomain(persistence)
|
||||
}
|
||||
|
||||
async removeByKey(key: string): Promise<void> {
|
||||
await this.ormRepository.createQueryBuilder().delete().where('key = :key', { key }).execute()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { CacheEntryRepositoryInterface, CacheEntry } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { EphemeralSession } from '../../Domain/Session/EphemeralSession'
|
||||
import { EphemeralSessionRepositoryInterface } from '../../Domain/Session/EphemeralSessionRepositoryInterface'
|
||||
|
||||
export class TypeORMEphemeralSessionRepository implements EphemeralSessionRepositoryInterface {
|
||||
private readonly PREFIX = 'session'
|
||||
private readonly USER_SESSIONS_PREFIX = 'user-sessions'
|
||||
|
||||
constructor(
|
||||
private cacheEntryRepository: CacheEntryRepositoryInterface,
|
||||
private ephemeralSessionAge: number,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async deleteOne(uuid: string, userUuid: string): Promise<void> {
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${uuid}`)
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${uuid}:${userUuid}`)
|
||||
|
||||
const userSessionsJSON = await this.cacheEntryRepository.findUnexpiredOneByKey(
|
||||
`${this.USER_SESSIONS_PREFIX}:${userUuid}`,
|
||||
)
|
||||
if (userSessionsJSON) {
|
||||
const userSessions = JSON.parse(userSessionsJSON.props.value)
|
||||
const updatedUserSessions = userSessions.filter((sessionUuid: string) => sessionUuid !== uuid)
|
||||
userSessionsJSON.props.value = JSON.stringify(updatedUserSessions)
|
||||
await this.cacheEntryRepository.save(userSessionsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
async updateTokensAndExpirationDates(
|
||||
uuid: string,
|
||||
hashedAccessToken: string,
|
||||
hashedRefreshToken: string,
|
||||
accessExpiration: Date,
|
||||
refreshExpiration: Date,
|
||||
): Promise<void> {
|
||||
const session = await this.findOneByUuid(uuid)
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
session.hashedAccessToken = hashedAccessToken
|
||||
session.hashedRefreshToken = hashedRefreshToken
|
||||
session.accessExpiration = accessExpiration
|
||||
session.refreshExpiration = refreshExpiration
|
||||
|
||||
await this.save(session)
|
||||
}
|
||||
|
||||
async findAllByUserUuid(userUuid: string): Promise<Array<EphemeralSession>> {
|
||||
const ephemeralSessionUuidsJSON = await this.cacheEntryRepository.findUnexpiredOneByKey(
|
||||
`${this.USER_SESSIONS_PREFIX}:${userUuid}`,
|
||||
)
|
||||
if (!ephemeralSessionUuidsJSON) {
|
||||
return []
|
||||
}
|
||||
const ephemeralSessionUuids = JSON.parse(ephemeralSessionUuidsJSON.props.value)
|
||||
|
||||
const sessions = []
|
||||
for (const ephemeralSessionUuid of ephemeralSessionUuids) {
|
||||
const stringifiedSession = await this.cacheEntryRepository.findUnexpiredOneByKey(
|
||||
`${this.PREFIX}:${ephemeralSessionUuid}`,
|
||||
)
|
||||
if (stringifiedSession !== null) {
|
||||
sessions.push(JSON.parse(stringifiedSession.props.value))
|
||||
}
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
async findOneByUuid(uuid: string): Promise<EphemeralSession | null> {
|
||||
const stringifiedSession = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${uuid}`)
|
||||
if (!stringifiedSession) {
|
||||
return null
|
||||
}
|
||||
|
||||
return JSON.parse(stringifiedSession.props.value)
|
||||
}
|
||||
|
||||
async findOneByUuidAndUserUuid(uuid: string, userUuid: string): Promise<EphemeralSession | null> {
|
||||
const stringifiedSession = await this.cacheEntryRepository.findUnexpiredOneByKey(
|
||||
`${this.PREFIX}:${uuid}:${userUuid}`,
|
||||
)
|
||||
if (!stringifiedSession) {
|
||||
return null
|
||||
}
|
||||
|
||||
return JSON.parse(stringifiedSession.props.value)
|
||||
}
|
||||
|
||||
async save(ephemeralSession: EphemeralSession): Promise<void> {
|
||||
const ttl = this.ephemeralSessionAge
|
||||
|
||||
const stringifiedSession = JSON.stringify(ephemeralSession)
|
||||
|
||||
await this.cacheEntryRepository.save(
|
||||
CacheEntry.create({
|
||||
key: `${this.PREFIX}:${ephemeralSession.uuid}:${ephemeralSession.userUuid}`,
|
||||
value: stringifiedSession,
|
||||
expiresAt: this.timer.getUTCDateNSecondsAhead(ttl),
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
await this.cacheEntryRepository.save(
|
||||
CacheEntry.create({
|
||||
key: `${this.PREFIX}:${ephemeralSession.uuid}`,
|
||||
value: stringifiedSession,
|
||||
expiresAt: this.timer.getUTCDateNSecondsAhead(ttl),
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
const ephemeralSessionUuidsJSON = await this.cacheEntryRepository.findUnexpiredOneByKey(
|
||||
`${this.USER_SESSIONS_PREFIX}:${ephemeralSession.userUuid}`,
|
||||
)
|
||||
if (!ephemeralSessionUuidsJSON) {
|
||||
await this.cacheEntryRepository.save(
|
||||
CacheEntry.create({
|
||||
key: `${this.USER_SESSIONS_PREFIX}:${ephemeralSession.userUuid}`,
|
||||
value: JSON.stringify([ephemeralSession.uuid]),
|
||||
expiresAt: this.timer.getUTCDateNSecondsAhead(ttl),
|
||||
}).getValue(),
|
||||
)
|
||||
} else {
|
||||
const ephemeralSessionUuids = JSON.parse(ephemeralSessionUuidsJSON.props.value)
|
||||
ephemeralSessionUuids.push(ephemeralSession.uuid)
|
||||
ephemeralSessionUuidsJSON.props.value = JSON.stringify(ephemeralSessionUuids)
|
||||
ephemeralSessionUuidsJSON.props.expiresAt = this.timer.getUTCDateNSecondsAhead(ttl)
|
||||
await this.cacheEntryRepository.save(ephemeralSessionUuidsJSON)
|
||||
}
|
||||
}
|
||||
}
|
||||
83
packages/auth/src/Infra/TypeORM/TypeORMLockRepository.ts
Normal file
83
packages/auth/src/Infra/TypeORM/TypeORMLockRepository.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { CacheEntryRepositoryInterface, CacheEntry } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { LockRepositoryInterface } from '../../Domain/User/LockRepositoryInterface'
|
||||
|
||||
export class TypeORMLockRepository implements LockRepositoryInterface {
|
||||
private readonly PREFIX = 'lock'
|
||||
private readonly OTP_PREFIX = 'otp-lock'
|
||||
|
||||
constructor(
|
||||
private cacheEntryRepository: CacheEntryRepositoryInterface,
|
||||
private timer: TimerInterface,
|
||||
private maxLoginAttempts: number,
|
||||
private failedLoginLockout: number,
|
||||
) {}
|
||||
|
||||
async lockSuccessfullOTP(userIdentifier: string, otp: string): Promise<void> {
|
||||
const cacheEntryOrError = CacheEntry.create({
|
||||
key: `${this.OTP_PREFIX}:${userIdentifier}`,
|
||||
value: otp,
|
||||
expiresAt: this.timer.getUTCDateNSecondsAhead(60),
|
||||
})
|
||||
if (cacheEntryOrError.isFailed()) {
|
||||
throw new Error('Could not create cache entry')
|
||||
}
|
||||
|
||||
await this.cacheEntryRepository.save(cacheEntryOrError.getValue())
|
||||
}
|
||||
|
||||
async isOTPLocked(userIdentifier: string, otp: string): Promise<boolean> {
|
||||
const lock = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.OTP_PREFIX}:${userIdentifier}`)
|
||||
if (!lock) {
|
||||
return false
|
||||
}
|
||||
|
||||
return lock.props.value === otp
|
||||
}
|
||||
|
||||
async resetLockCounter(userIdentifier: string): Promise<void> {
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${userIdentifier}`)
|
||||
}
|
||||
|
||||
async updateLockCounter(userIdentifier: string, counter: number): Promise<void> {
|
||||
let cacheEntry = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${userIdentifier}`)
|
||||
if (!cacheEntry) {
|
||||
cacheEntry = CacheEntry.create({
|
||||
key: `${this.PREFIX}:${userIdentifier}`,
|
||||
value: counter.toString(),
|
||||
expiresAt: this.timer.getUTCDateNSecondsAhead(this.failedLoginLockout),
|
||||
}).getValue()
|
||||
} else {
|
||||
cacheEntry.props.value = counter.toString()
|
||||
cacheEntry.props.expiresAt = this.timer.getUTCDateNSecondsAhead(this.failedLoginLockout)
|
||||
}
|
||||
|
||||
await this.cacheEntryRepository.save(cacheEntry)
|
||||
}
|
||||
|
||||
async getLockCounter(userIdentifier: string): Promise<number> {
|
||||
const counter = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${userIdentifier}`)
|
||||
|
||||
if (!counter) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return +counter.props.value
|
||||
}
|
||||
|
||||
async lockUser(userIdentifier: string): Promise<void> {
|
||||
const cacheEntry = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${userIdentifier}`)
|
||||
if (cacheEntry !== null) {
|
||||
cacheEntry.props.expiresAt = this.timer.getUTCDateNSecondsAhead(this.failedLoginLockout)
|
||||
|
||||
await this.cacheEntryRepository.save(cacheEntry)
|
||||
}
|
||||
}
|
||||
|
||||
async isUserLocked(userIdentifier: string): Promise<boolean> {
|
||||
const counter = await this.getLockCounter(userIdentifier)
|
||||
|
||||
return counter >= this.maxLoginAttempts
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CacheEntryRepositoryInterface, CacheEntry } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { OfflineSubscriptionToken } from '../../Domain/Auth/OfflineSubscriptionToken'
|
||||
import { OfflineSubscriptionTokenRepositoryInterface } from '../../Domain/Auth/OfflineSubscriptionTokenRepositoryInterface'
|
||||
|
||||
export class TypeORMOfflineSubscriptionTokenRepository implements OfflineSubscriptionTokenRepositoryInterface {
|
||||
private readonly PREFIX = 'offline-subscription-token'
|
||||
|
||||
constructor(private cacheEntryRepository: CacheEntryRepositoryInterface, private timer: TimerInterface) {}
|
||||
|
||||
async getUserEmailByToken(token: string): Promise<string | undefined> {
|
||||
const userUuid = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${token}`)
|
||||
if (!userUuid) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return userUuid.props.value
|
||||
}
|
||||
|
||||
async save(offlineSubscriptionToken: OfflineSubscriptionToken): Promise<void> {
|
||||
const key = `${this.PREFIX}:${offlineSubscriptionToken.token}`
|
||||
|
||||
await this.cacheEntryRepository.save(
|
||||
CacheEntry.create({
|
||||
key,
|
||||
value: offlineSubscriptionToken.userEmail,
|
||||
expiresAt: this.timer.convertMicrosecondsToDate(offlineSubscriptionToken.expiresAt),
|
||||
}).getValue(),
|
||||
)
|
||||
}
|
||||
}
|
||||
33
packages/auth/src/Infra/TypeORM/TypeORMPKCERepository.ts
Normal file
33
packages/auth/src/Infra/TypeORM/TypeORMPKCERepository.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { CacheEntry, CacheEntryRepositoryInterface } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { PKCERepositoryInterface } from '../../Domain/User/PKCERepositoryInterface'
|
||||
|
||||
export class TypeORMPKCERepository implements PKCERepositoryInterface {
|
||||
private readonly PREFIX = 'pkce'
|
||||
|
||||
constructor(
|
||||
private cacheEntryRepository: CacheEntryRepositoryInterface,
|
||||
private logger: Logger,
|
||||
private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async storeCodeChallenge(codeChallenge: string): Promise<void> {
|
||||
this.logger.debug(`Storing code challenge: ${codeChallenge}`)
|
||||
|
||||
await this.cacheEntryRepository.save(
|
||||
CacheEntry.create({
|
||||
key: `${this.PREFIX}:${codeChallenge}`,
|
||||
value: codeChallenge,
|
||||
expiresAt: this.timer.getUTCDateNSecondsAhead(3600),
|
||||
}).getValue(),
|
||||
)
|
||||
}
|
||||
|
||||
async removeCodeChallenge(codeChallenge: string): Promise<boolean> {
|
||||
await this.cacheEntryRepository.removeByKey(`${this.PREFIX}:${codeChallenge}`)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CacheEntryRepositoryInterface, CacheEntry } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { SubscriptionToken } from '../../Domain/Subscription/SubscriptionToken'
|
||||
import { SubscriptionTokenRepositoryInterface } from '../../Domain/Subscription/SubscriptionTokenRepositoryInterface'
|
||||
|
||||
export class TypeORMSubscriptionTokenRepository implements SubscriptionTokenRepositoryInterface {
|
||||
private readonly PREFIX = 'subscription-token'
|
||||
|
||||
constructor(private cacheEntryRepository: CacheEntryRepositoryInterface, private timer: TimerInterface) {}
|
||||
|
||||
async getUserUuidByToken(token: string): Promise<string | undefined> {
|
||||
const userUuid = await this.cacheEntryRepository.findUnexpiredOneByKey(`${this.PREFIX}:${token}`)
|
||||
if (!userUuid) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return userUuid.props.value
|
||||
}
|
||||
|
||||
async save(subscriptionToken: SubscriptionToken): Promise<boolean> {
|
||||
const key = `${this.PREFIX}:${subscriptionToken.token}`
|
||||
|
||||
await this.cacheEntryRepository.save(
|
||||
CacheEntry.create({
|
||||
key,
|
||||
value: subscriptionToken.userUuid,
|
||||
expiresAt: this.timer.convertMicrosecondsToDate(subscriptionToken.expiresAt),
|
||||
}).getValue(),
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
32
packages/auth/src/Mapping/CacheEntryPersistenceMapper.ts
Normal file
32
packages/auth/src/Mapping/CacheEntryPersistenceMapper.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { CacheEntry, MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
||||
|
||||
import { TypeORMCacheEntry } from '../Infra/TypeORM/TypeORMCacheEntry'
|
||||
|
||||
export class CacheEntryPersistenceMapper implements MapperInterface<CacheEntry, TypeORMCacheEntry> {
|
||||
toDomain(projection: TypeORMCacheEntry): CacheEntry {
|
||||
const cacheEntryOrError = CacheEntry.create(
|
||||
{
|
||||
key: projection.key,
|
||||
value: projection.value,
|
||||
expiresAt: projection.expiresAt,
|
||||
},
|
||||
new UniqueEntityId(projection.uuid),
|
||||
)
|
||||
if (cacheEntryOrError.isFailed()) {
|
||||
throw new Error(`CacheEntryPersistenceMapper.toDomain: ${cacheEntryOrError.getError()}`)
|
||||
}
|
||||
|
||||
return cacheEntryOrError.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: CacheEntry): TypeORMCacheEntry {
|
||||
const typeorm = new TypeORMCacheEntry()
|
||||
|
||||
typeorm.uuid = domain.id.toString()
|
||||
typeorm.key = domain.props.key
|
||||
typeorm.value = domain.props.value
|
||||
typeorm.expiresAt = domain.props.expiresAt
|
||||
|
||||
return typeorm
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.13.0...@standardnotes/domain-core@1.14.0) (2023-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
* extract cache entry model to domain-core ([#581](https://github.com/standardnotes/server/issues/581)) ([c71f7ff](https://github.com/standardnotes/server/commit/c71f7ff8ad4ffbd7151e8397b5816e383b178eb4))
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.12.0...@standardnotes/domain-core@1.13.0) (2023-04-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-core",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
18
packages/domain-core/src/Domain/Cache/CacheEntry.ts
Normal file
18
packages/domain-core/src/Domain/Cache/CacheEntry.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Entity } from '../Core/Entity'
|
||||
import { Result } from '../Core/Result'
|
||||
import { UniqueEntityId } from '../Core/UniqueEntityId'
|
||||
import { CacheEntryProps } from './CacheEntryProps'
|
||||
|
||||
export class CacheEntry extends Entity<CacheEntryProps> {
|
||||
get id(): UniqueEntityId {
|
||||
return this._id
|
||||
}
|
||||
|
||||
private constructor(props: CacheEntryProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
static create(props: CacheEntryProps, id?: UniqueEntityId): Result<CacheEntry> {
|
||||
return Result.ok<CacheEntry>(new CacheEntry(props, id))
|
||||
}
|
||||
}
|
||||
5
packages/domain-core/src/Domain/Cache/CacheEntryProps.ts
Normal file
5
packages/domain-core/src/Domain/Cache/CacheEntryProps.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface CacheEntryProps {
|
||||
key: string
|
||||
value: string
|
||||
expiresAt: Date | null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { CacheEntry } from './CacheEntry'
|
||||
|
||||
export interface CacheEntryRepositoryInterface {
|
||||
save(cacheEntry: CacheEntry): Promise<void>
|
||||
findUnexpiredOneByKey(key: string): Promise<CacheEntry | null>
|
||||
removeByKey(key: string): Promise<void>
|
||||
}
|
||||
@@ -5,6 +5,10 @@ export * from './Auth/SessionProps'
|
||||
export * from './Auth/SessionToken'
|
||||
export * from './Auth/SessionTokenProps'
|
||||
|
||||
export * from './Cache/CacheEntry'
|
||||
export * from './Cache/CacheEntryProps'
|
||||
export * from './Cache/CacheEntryRepositoryInterface'
|
||||
|
||||
export * from './Common/Dates'
|
||||
export * from './Common/DatesProps'
|
||||
export * from './Common/Email'
|
||||
|
||||
@@ -4,6 +4,7 @@ VERSION=development
|
||||
|
||||
PORT=3000
|
||||
|
||||
CACHE_TYPE=redis
|
||||
REDIS_URL=redis://cache
|
||||
|
||||
VALET_TOKEN_SECRET=change-me-!
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.11.0...@standardnotes/files-server@1.11.1) (2023-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add env vars to control cache type for home server ([c8ea2ab](https://github.com/standardnotes/files/commit/c8ea2ab199bfd6d1836078fa26d578400a8099db))
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.10.14...@standardnotes/files-server@1.11.0) (2023-05-02)
|
||||
|
||||
### Features
|
||||
|
||||
* **files:** add in memory upload repository for home server purposes ([#583](https://github.com/standardnotes/files/issues/583)) ([14ce6dd](https://github.com/standardnotes/files/commit/14ce6dd818f377d63156ad10353de7d193d443c3))
|
||||
|
||||
## [1.10.14](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.10.13...@standardnotes/files-server@1.10.14) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.10.13](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.10.12...@standardnotes/files-server@1.10.13) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.10.13",
|
||||
"version": "1.11.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
|
||||
import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
@@ -51,6 +52,8 @@ export class ContainerConfigLoader {
|
||||
|
||||
const container = new Container()
|
||||
|
||||
const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
|
||||
|
||||
const logger = this.createLogger({ env })
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
@@ -155,7 +158,13 @@ export class ContainerConfigLoader {
|
||||
container.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
|
||||
// repositories
|
||||
container.bind<UploadRepositoryInterface>(TYPES.UploadRepository).to(RedisUploadRepository)
|
||||
if (isConfiguredForHomeServer) {
|
||||
container
|
||||
.bind<UploadRepositoryInterface>(TYPES.UploadRepository)
|
||||
.toConstantValue(new InMemoryUploadRepository(container.get(TYPES.Timer)))
|
||||
} else {
|
||||
container.bind<UploadRepositoryInterface>(TYPES.UploadRepository).to(RedisUploadRepository)
|
||||
}
|
||||
|
||||
container
|
||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { UploadRepositoryInterface } from '../../Domain/Upload/UploadRepositoryInterface'
|
||||
import { UploadChunkResult } from '../../Domain/Upload/UploadChunkResult'
|
||||
|
||||
export class InMemoryUploadRepository implements UploadRepositoryInterface {
|
||||
private readonly UPLOAD_SESSION_PREFIX = 'upload-session'
|
||||
private readonly UPLOAD_CHUNKS_PREFIX = 'upload-chunks'
|
||||
private readonly UPLOAD_SESSION_DEFAULT_TTL = 7200
|
||||
|
||||
private inMemoryUploadCacheMap: Map<string, string> = new Map()
|
||||
private inMemoryUploadTTLMap: Map<string, number> = new Map()
|
||||
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
async storeUploadSession(filePath: string, uploadId: string): Promise<void> {
|
||||
this.inMemoryUploadCacheMap.set(`${this.UPLOAD_SESSION_PREFIX}:${filePath}`, uploadId)
|
||||
this.inMemoryUploadTTLMap.set(
|
||||
`${this.UPLOAD_SESSION_PREFIX}:${filePath}`,
|
||||
+this.timer.getUTCDateNSecondsAhead(this.UPLOAD_SESSION_DEFAULT_TTL) / 1000,
|
||||
)
|
||||
}
|
||||
|
||||
async retrieveUploadSessionId(filePath: string): Promise<string | undefined> {
|
||||
this.invalidateStaleUploads()
|
||||
|
||||
return this.inMemoryUploadCacheMap.get(`${this.UPLOAD_SESSION_PREFIX}:${filePath}`)
|
||||
}
|
||||
|
||||
async storeUploadChunkResult(uploadId: string, uploadChunkResult: UploadChunkResult): Promise<void> {
|
||||
this.invalidateStaleUploads()
|
||||
|
||||
let uploadResults = []
|
||||
const uploadResultsJSON = this.inMemoryUploadCacheMap.get(`${this.UPLOAD_CHUNKS_PREFIX}:${uploadId}`)
|
||||
if (uploadResultsJSON) {
|
||||
uploadResults = JSON.parse(uploadResultsJSON)
|
||||
}
|
||||
uploadResults.unshift(JSON.stringify(uploadChunkResult))
|
||||
|
||||
this.inMemoryUploadCacheMap.set(`${this.UPLOAD_CHUNKS_PREFIX}:${uploadId}`, JSON.stringify(uploadResults))
|
||||
this.inMemoryUploadTTLMap.set(
|
||||
`${this.UPLOAD_CHUNKS_PREFIX}:${uploadId}`,
|
||||
+this.timer.getUTCDateNSecondsAhead(this.UPLOAD_SESSION_DEFAULT_TTL) / 1000,
|
||||
)
|
||||
}
|
||||
|
||||
async retrieveUploadChunkResults(uploadId: string): Promise<UploadChunkResult[]> {
|
||||
this.invalidateStaleUploads()
|
||||
|
||||
let uploadResults = []
|
||||
const uploadResultsJSON = this.inMemoryUploadCacheMap.get(`${this.UPLOAD_CHUNKS_PREFIX}:${uploadId}`)
|
||||
if (uploadResultsJSON) {
|
||||
uploadResults = JSON.parse(uploadResultsJSON)
|
||||
}
|
||||
|
||||
const uploadChunksResults: UploadChunkResult[] = []
|
||||
for (const stringifiedUploadChunkResult of uploadResults) {
|
||||
uploadChunksResults.push(JSON.parse(stringifiedUploadChunkResult))
|
||||
}
|
||||
|
||||
const sortedResults = uploadChunksResults.sort((a, b) => {
|
||||
return a.chunkId - b.chunkId
|
||||
})
|
||||
|
||||
return sortedResults
|
||||
}
|
||||
|
||||
private invalidateStaleUploads(): void {
|
||||
const now = this.timer.getTimestampInSeconds()
|
||||
for (const [key, value] of this.inMemoryUploadTTLMap.entries()) {
|
||||
if (value < now) {
|
||||
this.inMemoryUploadCacheMap.delete(key)
|
||||
this.inMemoryUploadTTLMap.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
## [1.13.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.13.0...@standardnotes/revisions-server@1.13.1) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.12.16...@standardnotes/revisions-server@1.13.0) (2023-04-28)
|
||||
|
||||
### Features
|
||||
|
||||
* **revisions:** add sqlite driver ([#575](https://github.com/standardnotes/server/issues/575)) ([03f9c60](https://github.com/standardnotes/server/commit/03f9c6039c309fde1a762a010e70e8189f5a8e15))
|
||||
|
||||
## [1.12.16](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.12.15...@standardnotes/revisions-server@1.12.16) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class initialBoilerplate1682678053275 implements MigrationInterface {
|
||||
name = 'initialBoilerplate1682678053275'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE "revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
|
||||
)
|
||||
await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
|
||||
await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX "user_uuid"')
|
||||
await queryRunner.query('DROP INDEX "item_uuid"')
|
||||
await queryRunner.query('DROP TABLE "revisions"')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.12.16",
|
||||
"version": "1.13.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -35,6 +35,7 @@
|
||||
"@standardnotes/responses": "^1.13.9",
|
||||
"@standardnotes/security": "workspace:^",
|
||||
"@standardnotes/time": "workspace:^",
|
||||
"better-sqlite3": "^8.3.0",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -47,6 +48,7 @@
|
||||
"winston": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
"@types/cors": "^2.8.9",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/express": "^4.17.14",
|
||||
|
||||
@@ -2,10 +2,11 @@ import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { Container, interfaces } from 'inversify'
|
||||
import { Repository } from 'typeorm'
|
||||
import * as winston from 'winston'
|
||||
|
||||
import { Revision } from '../Domain/Revision/Revision'
|
||||
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||
import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
|
||||
import { MySQLRevisionRepository } from '../Infra/MySQL/MySQLRevisionRepository'
|
||||
import { TypeORMRevisionRepository } from '../Infra/TypeORM/TypeORMRevisionRepository'
|
||||
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
|
||||
import { RevisionMetadataPersistenceMapper } from '../Mapping/RevisionMetadataPersistenceMapper'
|
||||
import { RevisionPersistenceMapper } from '../Mapping/RevisionPersistenceMapper'
|
||||
@@ -67,7 +68,7 @@ export class CommonContainerConfigLoader {
|
||||
container
|
||||
.bind<RevisionRepositoryInterface>(TYPES.RevisionRepository)
|
||||
.toDynamicValue((context: interfaces.Context) => {
|
||||
return new MySQLRevisionRepository(
|
||||
return new TypeORMRevisionRepository(
|
||||
context.container.get(TYPES.ORMRevisionRepository),
|
||||
context.container.get(TYPES.RevisionMetadataPersistenceMapper),
|
||||
context.container.get(TYPES.RevisionPersistenceMapper),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { DataSource, LoggerOptions } from 'typeorm'
|
||||
import { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions'
|
||||
import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
|
||||
|
||||
import { TypeORMRevision } from '../Infra/TypeORM/TypeORMRevision'
|
||||
|
||||
@@ -7,6 +9,8 @@ import { Env } from './Env'
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const isConfiguredForMySQL = env.get('DB_TYPE') === 'mysql'
|
||||
|
||||
const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
: 45_000
|
||||
@@ -34,22 +38,32 @@ const replicationConfig = {
|
||||
restoreNodeTimeout: 5,
|
||||
}
|
||||
|
||||
const dataSource = new DataSource({
|
||||
const commonDataSourceOptions = {
|
||||
maxQueryExecutionTime,
|
||||
entities: [TypeORMRevision],
|
||||
migrations: [`dist/migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
|
||||
}
|
||||
|
||||
const mySQLDataSourceOptions: MysqlConnectionOptions = {
|
||||
...commonDataSourceOptions,
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
replication: inReplicaMode ? replicationConfig : undefined,
|
||||
host: inReplicaMode ? undefined : env.get('DB_HOST'),
|
||||
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
|
||||
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
|
||||
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
|
||||
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
|
||||
entities: [TypeORMRevision],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
|
||||
})
|
||||
}
|
||||
|
||||
export const AppDataSource = dataSource
|
||||
const sqliteDataSourceOptions: BetterSqlite3ConnectionOptions = {
|
||||
...commonDataSourceOptions,
|
||||
type: 'better-sqlite3',
|
||||
database: `data/${env.get('DB_DATABASE')}.sqlite`,
|
||||
}
|
||||
|
||||
export const AppDataSource = new DataSource(isConfiguredForMySQL ? mySQLDataSourceOptions : sqliteDataSourceOptions)
|
||||
|
||||
@@ -22,7 +22,7 @@ export class TypeORMRevision {
|
||||
declare userUuid: string | null
|
||||
|
||||
@Column({
|
||||
type: 'mediumtext',
|
||||
type: 'text',
|
||||
nullable: true,
|
||||
})
|
||||
declare content: string | null
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Logger } from 'winston'
|
||||
import { Revision } from '../../Domain/Revision/Revision'
|
||||
import { RevisionMetadata } from '../../Domain/Revision/RevisionMetadata'
|
||||
import { RevisionRepositoryInterface } from '../../Domain/Revision/RevisionRepositoryInterface'
|
||||
import { TypeORMRevision } from '../TypeORM/TypeORMRevision'
|
||||
import { TypeORMRevision } from './TypeORMRevision'
|
||||
|
||||
export class MySQLRevisionRepository implements RevisionRepositoryInterface {
|
||||
export class TypeORMRevisionRepository implements RevisionRepositoryInterface {
|
||||
constructor(
|
||||
private ormRepository: Repository<TypeORMRevision>,
|
||||
private revisionMetadataMapper: MapperInterface<RevisionMetadata, TypeORMRevision>,
|
||||
@@ -97,7 +97,7 @@ export class MySQLRevisionRepository implements RevisionRepositoryInterface {
|
||||
const simplifiedRevisions = await queryBuilder.getRawMany()
|
||||
|
||||
this.logger.debug(
|
||||
`Found ${simplifiedRevisions.length} revisions MySQL entries for item ${itemUuid.value}`,
|
||||
`Found ${simplifiedRevisions.length} revisions entries for item ${itemUuid.value}`,
|
||||
simplifiedRevisions,
|
||||
)
|
||||
|
||||
@@ -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.17.14](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.17.13...@standardnotes/scheduler-server@1.17.14) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.17.13](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.17.12...@standardnotes/scheduler-server@1.17.13) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.17.13",
|
||||
"version": "1.17.14",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.21.1](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.21.0...@standardnotes/settings@1.21.1) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/settings
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/server/compare/@standardnotes/settings@1.20.2...@standardnotes/settings@1.21.0) (2023-04-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/settings",
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.34.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.34.0...@standardnotes/syncing-server@1.34.1) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
# [1.34.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.33.0...@standardnotes/syncing-server@1.34.0) (2023-04-27)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.34.0",
|
||||
"version": "1.34.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.6.15](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.6.14...@standardnotes/websockets-server@1.6.15) (2023-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.6.14](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.6.13...@standardnotes/websockets-server@1.6.14) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.6.14",
|
||||
"version": "1.6.15",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3604,6 +3604,7 @@ __metadata:
|
||||
"@standardnotes/responses": "npm:^1.13.9"
|
||||
"@standardnotes/security": "workspace:^"
|
||||
"@standardnotes/time": "workspace:^"
|
||||
"@types/better-sqlite3": "npm:^7.6.4"
|
||||
"@types/cors": "npm:^2.8.9"
|
||||
"@types/dotenv": "npm:^8.2.0"
|
||||
"@types/express": "npm:^4.17.14"
|
||||
@@ -3611,6 +3612,7 @@ __metadata:
|
||||
"@types/jest": "npm:^29.1.1"
|
||||
"@types/newrelic": "npm:^9.13.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^5.48.2"
|
||||
better-sqlite3: "npm:^8.3.0"
|
||||
cors: "npm:2.8.5"
|
||||
dotenv: "npm:^16.0.1"
|
||||
eslint: "npm:^8.32.0"
|
||||
|
||||
Reference in New Issue
Block a user