Compare commits

..

6 Commits

11 changed files with 192 additions and 8 deletions

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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.49.13",
"version": "1.50.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -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('DB_TYPE') === 'sqlite'
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
}
}

View File

@@ -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)
}
}
}
}

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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.103.0",
"version": "1.103.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -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')

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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.10.14",
"version": "1.11.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

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('DB_TYPE') === 'sqlite'
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)

View File

@@ -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)
}
}
}
}