Compare commits

..

21 Commits

Author SHA1 Message Date
standardci 093cc07c39 chore(release): publish new version
- @standardnotes/api-gateway@1.50.1
 - @standardnotes/files-server@1.11.1
2023-05-04 10:36:26 +00:00
Karol Sójko 30f14820c6 feat: add cache type to e2e test suite 2023-05-04 12:20:37 +02:00
Karol Sójko c8ea2ab199 fix: add env vars to control cache type for home server 2023-05-04 12:11:19 +02:00
standardci d56bbacc0b chore(release): publish new version
- @standardnotes/auth-server@1.103.1
2023-05-04 09:39:16 +00:00
Karol Sójko bb468a8b7e fix(auth): invalidate cross service token cache upon shared subscription accepting (#586) 2023-05-04 11:25:12 +02:00
standardci 7e99f4b078 chore(release): publish new version
- @standardnotes/files-server@1.11.0
2023-05-02 11:19:22 +00:00
Karol Sójko 14ce6dd818 feat(files): add in memory upload repository for home server purposes (#583) 2023-05-02 13:02:09 +02:00
standardci 063a3e425d chore(release): publish new version
- @standardnotes/api-gateway@1.50.0
2023-05-02 10:13:47 +00:00
Karol Sójko 0900dc75ac feat(api-gateway): add in memory cache for home server (#582) 2023-05-02 11:58:38 +02:00
standardci aa8bd1f8dc chore(release): publish new version
- @standardnotes/analytics@2.21.10
 - @standardnotes/api-gateway@1.49.13
 - @standardnotes/auth-server@1.103.0
 - @standardnotes/domain-core@1.14.0
 - @standardnotes/files-server@1.10.14
 - @standardnotes/revisions-server@1.13.1
 - @standardnotes/scheduler-server@1.17.14
 - @standardnotes/settings@1.21.1
 - @standardnotes/syncing-server@1.34.1
 - @standardnotes/websockets-server@1.6.15
2023-05-02 09:57:55 +00:00
Karol Sójko c71f7ff8ad feat: extract cache entry model to domain-core (#581)
* feat: extract cache entry model to domain-core

* fix(auth): rename cache table to be auth specific
2023-05-02 11:43:50 +02:00
standardci fe18420913 chore(release): publish new version
- @standardnotes/auth-server@1.102.0
2023-05-01 13:07:13 +00:00
Karol Sójko 97124928df feat(auth): add sqlite driver repositories (#580)
* feat(auth): add pkce sqlite repository for home server

* feat(auth): add sqlite subscription token repository for home server
2023-05-01 14:50:36 +02:00
standardci c108bfb12f chore(release): publish new version
- @standardnotes/auth-server@1.101.0
2023-05-01 12:05:53 +00:00
Karol Sójko 5fe6ed1462 feat(auth): add sqlite offline subscription token repository for home server (#579) 2023-05-01 13:50:24 +02:00
standardci df5fcce769 chore(release): publish new version
- @standardnotes/auth-server@1.100.0
2023-05-01 11:26:33 +00:00
Karol Sójko 8f57ece7b8 feat(auth): add sqlite ephemeral session repository for home server (#578) 2023-05-01 13:09:47 +02:00
standardci 8a10d201c5 chore(release): publish new version
- @standardnotes/auth-server@1.99.0
2023-05-01 10:16:44 +00:00
Karol Sójko 9d7e63a7a7 feat(auth): add sqlite lock cache for home server (#577)
* feat(auth): add sqlite lock cache for home server

* fix(auth): lock repository binding
2023-05-01 12:02:52 +02:00
standardci 87c1ae2ac0 chore(release): publish new version
- @standardnotes/auth-server@1.98.0
2023-05-01 08:03:44 +00:00
Karol Sójko 56c922e715 feat(auth): add cache entries model (#576) 2023-05-01 09:49:27 +02:00
48 changed files with 883 additions and 27 deletions
+2
View File
@@ -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
+3
View File
@@ -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"
#########
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.21.9",
"version": "2.21.10",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+1
View File
@@ -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
+16
View File
@@ -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 -1
View File
@@ -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)
}
}
}
}
+42
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.97.0",
"version": "1.103.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+83 -12
View File
@@ -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,
+3
View File
@@ -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')
@@ -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)
}
}
}
@@ -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(),
)
}
}
@@ -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
}
}
@@ -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
}
}
+6
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.13.0",
"version": "1.14.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -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))
}
}
@@ -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>
}
+4
View File
@@ -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'
+1
View File
@@ -4,6 +4,7 @@ VERSION=development
PORT=3000
CACHE_TYPE=redis
REDIS_URL=redis://cache
VALET_TOKEN_SECRET=change-me-!
+16
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.10.13",
"version": "1.11.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+10 -1
View File
@@ -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)
}
}
}
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.13.0",
"version": "1.13.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.17.13",
"version": "1.17.14",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/settings",
"version": "1.21.0",
"version": "1.21.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.34.0",
"version": "1.34.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.6.14",
"version": "1.6.15",
"engines": {
"node": ">=18.0.0 <19.0.0"
},