Compare commits

..

25 Commits

Author SHA1 Message Date
standardci
a1455d281f chore(release): publish new version
- @standardnotes/api-gateway@1.89.0
 - @standardnotes/auth-server@1.176.5
 - @standardnotes/home-server@1.22.20
 - @standardnotes/syncing-server@1.129.0
2023-12-12 12:18:49 +00:00
Karol Sójko
cfbe2bbac6 fix(auth): add user uuid context to sign in emails log context 2023-12-12 12:58:11 +01:00
Karol Sójko
398c10ce4b feat(syncing-server): add extended revisions frequency for free users (#965) 2023-12-12 12:42:31 +01:00
standardci
c7d21b092d chore(release): publish new version
- @standardnotes/auth-server@1.176.4
 - @standardnotes/home-server@1.22.19
2023-12-11 13:20:06 +00:00
Karol Sójko
031fa71e7d fix(auth): generate new recovery codes when enabling mfa (#964) 2023-12-11 13:59:57 +01:00
standardci
948e843ad6 chore(release): publish new version
- @standardnotes/analytics@2.34.6
 - @standardnotes/api-gateway@1.88.4
 - @standardnotes/auth-server@1.176.3
 - @standardnotes/domain-events-infra@1.22.5
 - @standardnotes/domain-events@2.138.2
 - @standardnotes/files-server@1.36.6
 - @standardnotes/home-server@1.22.18
 - @standardnotes/revisions-server@1.51.6
 - @standardnotes/scheduler-server@1.27.11
 - @standardnotes/syncing-server@1.128.2
 - @standardnotes/websockets-server@1.22.2
2023-12-11 11:25:27 +00:00
Karol Sójko
7b0ea0a069 fix(syncing-server): add user uuid for the emails requesting backup 2023-12-11 12:05:16 +01:00
standardci
8887b6e642 chore(release): publish new version
- @standardnotes/auth-server@1.176.2
 - @standardnotes/home-server@1.22.17
 - @standardnotes/syncing-server@1.128.1
2023-12-11 10:15:36 +00:00
Karol Sójko
597ff13393 fix(syncing-server): logs meta for email with backup requested 2023-12-11 10:55:16 +01:00
Karol Sójko
4ab61b94a4 fix(auth): error log meta on triggering email backups 2023-12-11 10:52:17 +01:00
standardci
e19652d62a chore(release): publish new version
- @standardnotes/api-gateway@1.88.3
 - @standardnotes/home-server@1.22.16
2023-12-08 16:15:29 +00:00
Karol Sójko
a341e78909 fix(api-gateway): add extra meta to logs 2023-12-08 16:55:18 +01:00
standardci
48e52ac48c chore(release): publish new version
- @standardnotes/home-server@1.22.15
 - @standardnotes/syncing-server@1.128.0
2023-12-08 12:30:03 +00:00
Karol Sójko
6dbb87708f feat(syncing-server): send websocket event to shared vault members upon items change in shared vault (#961) 2023-12-08 13:09:35 +01:00
standardci
d15d51eae6 chore(release): publish new version
- @standardnotes/api-gateway@1.88.2
 - @standardnotes/home-server@1.22.14
2023-12-07 13:48:28 +00:00
Karol Sójko
0058368681 fix(api-gateway): add userId to logs in error handler if possible 2023-12-07 14:28:12 +01:00
standardci
746c821698 chore(release): publish new version
- @standardnotes/analytics@2.34.5
 - @standardnotes/api-gateway@1.88.1
 - @standardnotes/auth-server@1.176.1
 - @standardnotes/files-server@1.36.5
 - @standardnotes/home-server@1.22.13
 - @standardnotes/revisions-server@1.51.5
 - @standardnotes/scheduler-server@1.27.10
 - @standardnotes/syncing-server@1.127.9
 - @standardnotes/websockets-server@1.22.1
2023-12-07 13:03:23 +00:00
Karol Sójko
3f2d8c902c fix(auth): logger meta on disabling settings 2023-12-07 13:43:21 +01:00
Karol Sójko
3637db2563 fix(api-gateway): response header on grpc websocket connection validation 2023-12-07 13:41:28 +01:00
Karol Sójko
8ac84c59af fix: logs meta 2023-12-07 13:39:30 +01:00
Karol Sójko
a2b1323568 fix: logger meta information 2023-12-07 13:29:36 +01:00
standardci
d35391288d chore(release): publish new version
- @standardnotes/api-gateway@1.88.0
 - @standardnotes/auth-server@1.176.0
 - @standardnotes/grpc@1.4.0
 - @standardnotes/home-server@1.22.12
 - @standardnotes/syncing-server@1.127.8
 - @standardnotes/websockets-server@1.22.0
2023-12-07 12:18:19 +00:00
Karol Sójko
d5c1b76de0 feat: replace websocket connection validation with grpc (#954)
* feat: replace websocket connection validation with grpc

* fix(api-gateway): error logs metadata details

* fix binding

* fix logs severity on websockets

* add user uuid to grpc call error logs
2023-12-07 12:58:07 +01:00
standardci
4600a49e88 chore(release): publish new version
- @standardnotes/websockets-server@1.21.4
2023-12-07 10:22:00 +00:00
Karol Sójko
96a2a67aa6 fix(websockets): remove connection trace when it is gone (#953) 2023-12-07 11:01:47 +01:00
84 changed files with 1368 additions and 101 deletions

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.34.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.5...@standardnotes/analytics@2.34.6) (2023-12-11)
**Note:** Version bump only for package @standardnotes/analytics
## [2.34.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.4...@standardnotes/analytics@2.34.5) (2023-12-07)
### Bug Fixes
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
## [2.34.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.3...@standardnotes/analytics@2.34.4) (2023-12-01)
### Bug Fixes

View File

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

View File

@@ -84,6 +84,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: 'analytics' },
})
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.89.0](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.4...@standardnotes/api-gateway@1.89.0) (2023-12-12)
### Features
* **syncing-server:** add extended revisions frequency for free users ([#965](https://github.com/standardnotes/server/issues/965)) ([398c10c](https://github.com/standardnotes/server/commit/398c10ce4b8e357728a8b4f354b3bf6ccc8e438d))
## [1.88.4](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.3...@standardnotes/api-gateway@1.88.4) (2023-12-11)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.88.3](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.2...@standardnotes/api-gateway@1.88.3) (2023-12-08)
### Bug Fixes
* **api-gateway:** add extra meta to logs ([a341e78](https://github.com/standardnotes/server/commit/a341e789093556f09c2a337e39a8053abdcf587b))
## [1.88.2](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.1...@standardnotes/api-gateway@1.88.2) (2023-12-07)
### Bug Fixes
* **api-gateway:** add userId to logs in error handler if possible ([0058368](https://github.com/standardnotes/server/commit/005836868126ae5fa4c4468644704938aea0f4ec))
## [1.88.1](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.88.0...@standardnotes/api-gateway@1.88.1) (2023-12-07)
### Bug Fixes
* **api-gateway:** response header on grpc websocket connection validation ([3637db2](https://github.com/standardnotes/server/commit/3637db2563255aaddd44700c039495c6b9a9e4aa))
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
# [1.88.0](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.87.7...@standardnotes/api-gateway@1.88.0) (2023-12-07)
### Features
* replace websocket connection validation with grpc ([#954](https://github.com/standardnotes/server/issues/954)) ([d5c1b76](https://github.com/standardnotes/server/commit/d5c1b76de068a64b334c4347cbefa973447a0f60))
## [1.87.7](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.87.6...@standardnotes/api-gateway@1.87.7) (2023-12-07)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -91,9 +91,13 @@ void container.load().then((container) => {
server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
logger.error(
`[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${request.headers['x-application-version']}] Error thrown: ${error.stack}`,
)
logger.error(`${error.stack}`, {
method: request.method,
url: request.url,
snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-version'],
userId: response.locals.user ? response.locals.user.uuid : undefined,
})
logger.debug(
`[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${
request.headers['x-application-version']

View File

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

View File

@@ -22,19 +22,13 @@ import { EndpointResolver } from '../Service/Resolver/EndpointResolver'
import { RequiredCrossServiceTokenMiddleware } from '../Controller/RequiredCrossServiceTokenMiddleware'
import { OptionalCrossServiceTokenMiddleware } from '../Controller/OptionalCrossServiceTokenMiddleware'
import { Transform } from 'stream'
import {
ISessionsClient,
ISyncingClient,
SessionsClient,
SyncRequest,
SyncResponse,
SyncingClient,
} from '@standardnotes/grpc'
import { AuthClient, IAuthClient, ISyncingClient, SyncRequest, SyncResponse, SyncingClient } from '@standardnotes/grpc'
import { GRPCServiceProxy } from '../Service/gRPC/GRPCServiceProxy'
import { GRPCSyncingServerServiceProxy } from '../Service/gRPC/GRPCSyncingServerServiceProxy'
import { SyncResponseHttpRepresentation } from '../Mapping/Sync/Http/SyncResponseHttpRepresentation'
import { SyncRequestGRPCMapper } from '../Mapping/Sync/GRPC/SyncRequestGRPCMapper'
import { SyncResponseGRPCMapper } from '../Mapping/Sync/GRPC/SyncResponseGRPCMapper'
import { GRPCWebSocketAuthMiddleware } from '../Controller/GRPCWebSocketAuthMiddleware'
export class ContainerConfigLoader {
async load(configuration?: {
@@ -51,6 +45,7 @@ export class ContainerConfigLoader {
const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted'
const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting
const isConfiguredForInMemoryCache = env.get('CACHE_TYPE', true) === 'memory'
const isConfiguredForGRPCProxy = env.get('SERVICE_PROXY_TYPE', true) === 'grpc'
container
.bind<boolean>(TYPES.ApiGateway_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING)
@@ -122,7 +117,6 @@ export class ContainerConfigLoader {
container
.bind<OptionalCrossServiceTokenMiddleware>(TYPES.ApiGateway_OptionalCrossServiceTokenMiddleware)
.to(OptionalCrossServiceTokenMiddleware)
container.bind<WebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
container
.bind<SubscriptionTokenAuthMiddleware>(TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
.to(SubscriptionTokenAuthMiddleware)
@@ -153,7 +147,6 @@ export class ContainerConfigLoader {
new DirectCallServiceProxy(configuration.serviceContainer, container.get(TYPES.ApiGateway_FILES_SERVER_URL)),
)
} else {
const isConfiguredForGRPCProxy = env.get('SERVICE_PROXY_TYPE', true) === 'grpc'
if (isConfiguredForGRPCProxy) {
container.bind(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL).toConstantValue(env.get('AUTH_SERVER_GRPC_URL'))
container.bind(TYPES.ApiGateway_SYNCING_SERVER_GRPC_URL).toConstantValue(env.get('SYNCING_SERVER_GRPC_URL'))
@@ -165,8 +158,8 @@ export class ContainerConfigLoader {
? +env.get('GRPC_MAX_MESSAGE_SIZE', true)
: 1024 * 1024 * 50
container.bind<ISessionsClient>(TYPES.ApiGateway_GRPCSessionsClient).toConstantValue(
new SessionsClient(
container.bind<IAuthClient>(TYPES.ApiGateway_GRPCAuthClient).toConstantValue(
new AuthClient(
container.get<string>(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL),
grpc.credentials.createInsecure(),
{
@@ -229,7 +222,7 @@ export class ContainerConfigLoader {
container.get<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache),
container.get<winston.Logger>(TYPES.ApiGateway_Logger),
container.get<TimerInterface>(TYPES.ApiGateway_Timer),
container.get<ISessionsClient>(TYPES.ApiGateway_GRPCSessionsClient),
container.get<IAuthClient>(TYPES.ApiGateway_GRPCAuthClient),
container.get<GRPCSyncingServerServiceProxy>(TYPES.ApiGateway_GRPCSyncingServerServiceProxy),
),
)
@@ -238,6 +231,20 @@ export class ContainerConfigLoader {
}
}
if (isConfiguredForGRPCProxy) {
container
.bind<GRPCWebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware)
.toConstantValue(
new GRPCWebSocketAuthMiddleware(
container.get<IAuthClient>(TYPES.ApiGateway_GRPCAuthClient),
container.get<string>(TYPES.ApiGateway_AUTH_JWT_SECRET),
container.get<winston.Logger>(TYPES.ApiGateway_Logger),
),
)
} else {
container.bind<WebSocketAuthMiddleware>(TYPES.ApiGateway_WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
}
logger.debug('Configuration complete')
return container

View File

@@ -34,6 +34,6 @@ export const TYPES = {
ApiGateway_CrossServiceTokenCache: Symbol.for('ApiGateway_CrossServiceTokenCache'),
ApiGateway_Timer: Symbol.for('ApiGateway_Timer'),
ApiGateway_EndpointResolver: Symbol.for('ApiGateway_EndpointResolver'),
ApiGateway_GRPCSessionsClient: Symbol.for('ApiGateway_GRPCSessionsClient'),
ApiGateway_GRPCAuthClient: Symbol.for('ApiGateway_GRPCAuthClient'),
ApiGateway_GRPCSyncingClient: Symbol.for('ApiGateway_GRPCSyncingClient'),
}

View File

@@ -0,0 +1,120 @@
import { CrossServiceTokenData } from '@standardnotes/security'
import * as grpc from '@grpc/grpc-js'
import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { Logger } from 'winston'
import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc'
import { RoleName } from '@standardnotes/domain-core'
export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
constructor(
private authClient: IAuthClient,
private jwtSecret: string,
private logger: Logger,
) {
super()
}
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
const authHeaderValue = request.headers.authorization as string
if (!authHeaderValue) {
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid login credentials.',
},
})
return
}
const promise = new Promise((resolve, reject) => {
try {
const request = new WebsocketConnectionAuthorizationHeader()
request.setToken(authHeaderValue)
this.authClient.validateWebsocket(
request,
(error: grpc.ServiceError | null, response: ConnectionValidationResponse) => {
if (error) {
const responseCode = error.metadata.get('x-auth-error-response-code').pop()
if (responseCode) {
return resolve({
status: +responseCode,
data: {
error: {
message: error.metadata.get('x-auth-error-message').pop(),
tag: error.metadata.get('x-auth-error-tag').pop(),
},
},
headers: {
contentType: 'application/json',
},
})
}
return reject(error)
}
return resolve({
status: 200,
data: {
authToken: response.getCrossServiceToken(),
},
headers: {
contentType: 'application/json',
},
})
},
)
} catch (error) {
return reject(error)
}
})
try {
const authResponse = (await promise) as {
status: number
headers: Record<string, unknown>
data: Record<string, unknown>
}
if (authResponse.status > 200) {
response.setHeader('content-type', 'application/json')
response.status(authResponse.status).send(authResponse.data)
return
}
const crossServiceToken = authResponse.data.authToken as string
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user
response.locals.session = decodedToken.session
response.locals.roles = decodedToken.roles
response.locals.isFreeUser =
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
} catch (error) {
this.logger.error(
`Could not pass the request to websocket connection validation on underlying service: ${
(error as Error).message
}`,
)
response
.status(500)
.send(
"Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
)
return
}
return next()
}
}

View File

@@ -240,6 +240,9 @@ export class HttpServiceProxy implements ServiceProxyInterface {
tooManyRetryAttempts
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
{
userId: response.locals.user ? response.locals.user.uuid : undefined,
},
)
this.logger.debug(`Response error: ${JSON.stringify(error)}`)

View File

@@ -2,7 +2,7 @@ import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
import { Request, Response } from 'express'
import { Logger } from 'winston'
import { TimerInterface } from '@standardnotes/time'
import { ISessionsClient, AuthorizationHeader, SessionValidationResponse } from '@standardnotes/grpc'
import { IAuthClient, AuthorizationHeader, SessionValidationResponse } from '@standardnotes/grpc'
import * as grpc from '@grpc/grpc-js'
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
@@ -23,7 +23,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
private crossServiceTokenCache: CrossServiceTokenCacheInterface,
private logger: Logger,
private timer: TimerInterface,
private sessionsClient: ISessionsClient,
private authClient: IAuthClient,
private gRPCSyncingServerServiceProxy: GRPCSyncingServerServiceProxy,
) {}
@@ -41,7 +41,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
this.logger.debug('[GRPCServiceProxy] Validating session via gRPC')
this.sessionsClient.validate(
this.authClient.validate(
request,
metadata,
(error: grpc.ServiceError | null, response: SessionValidationResponse) => {
@@ -275,6 +275,9 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
tooManyRetryAttempts
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
{
userId: response.locals.user ? response.locals.user.uuid : undefined,
},
)
this.logger.debug(`Response error: ${JSON.stringify(error)}`)

View File

@@ -31,6 +31,7 @@ export class GRPCSyncingServerServiceProxy {
if (response.locals.session) {
metadata.set('x-session-uuid', response.locals.session.uuid)
}
metadata.set('x-is-free-user', response.locals.isFreeUser ? 'true' : 'false')
this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
if (error) {
@@ -43,11 +44,10 @@ export class GRPCSyncingServerServiceProxy {
}
if (error.code === Status.INTERNAL) {
this.logger.error(
`[GRPCSyncingServerServiceProxy] Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(
payload,
)}`,
)
this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, {
codeTag: 'GRPCSyncingServerServiceProxy',
userId: response.locals.user.uuid,
})
}
return reject(error)
@@ -60,11 +60,10 @@ export class GRPCSyncingServerServiceProxy {
'code' in (error as Record<string, unknown>) &&
(error as Record<string, unknown>).code === Status.INTERNAL
) {
this.logger.error(
`[GRPCSyncingServerServiceProxy] Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(
payload,
)}`,
)
this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, {
codeTag: 'GRPCSyncingServerServiceProxy.catch',
userId: response.locals.user.uuid,
})
}
reject(error)

View File

@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.176.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.4...@standardnotes/auth-server@1.176.5) (2023-12-12)
### Bug Fixes
* **auth:** add user uuid context to sign in emails log context ([cfbe2bb](https://github.com/standardnotes/server/commit/cfbe2bbac60e9014d7ba0967e4b996fba7dc8629))
## [1.176.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.3...@standardnotes/auth-server@1.176.4) (2023-12-11)
### Bug Fixes
* **auth:** generate new recovery codes when enabling mfa ([#964](https://github.com/standardnotes/server/issues/964)) ([031fa71](https://github.com/standardnotes/server/commit/031fa71e7d86221ec7fb0f4b21c62454646564e2))
## [1.176.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.2...@standardnotes/auth-server@1.176.3) (2023-12-11)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.176.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.1...@standardnotes/auth-server@1.176.2) (2023-12-11)
### Bug Fixes
* **auth:** error log meta on triggering email backups ([4ab61b9](https://github.com/standardnotes/server/commit/4ab61b94a4aee361399a76c9f2b6b977c4832b06))
## [1.176.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.0...@standardnotes/auth-server@1.176.1) (2023-12-07)
### Bug Fixes
* **auth:** logger meta on disabling settings ([3f2d8c9](https://github.com/standardnotes/server/commit/3f2d8c902c5331e07959f0b7b42684ef47346220))
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
* logs meta ([8ac84c5](https://github.com/standardnotes/server/commit/8ac84c59af886bb9c42de012fb1e7864e116ab55))
# [1.176.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.175.1...@standardnotes/auth-server@1.176.0) (2023-12-07)
### Features
* replace websocket connection validation with grpc ([#954](https://github.com/standardnotes/server/issues/954)) ([d5c1b76](https://github.com/standardnotes/server/commit/d5c1b76de068a64b334c4347cbefa973447a0f60))
## [1.175.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.175.0...@standardnotes/auth-server@1.175.1) (2023-12-07)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -30,10 +30,11 @@ import { InversifyExpressServer } from 'inversify-express-utils'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { SessionsServer } from '../src/Infra/gRPC/SessionsServer'
import { SessionsService } from '@standardnotes/grpc'
import { AuthServer } from '../src/Infra/gRPC/AuthServer'
import { AuthService } from '@standardnotes/grpc'
import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest'
import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
const container = new ContainerConfigLoader()
void container.load().then((container) => {
@@ -95,14 +96,16 @@ void container.load().then((container) => {
const gRPCPort = env.get('GRPC_PORT', true) ? +env.get('GRPC_PORT', true) : 50051
const sessionsServer = new SessionsServer(
const authServer = new AuthServer(
container.get<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest),
container.get<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken),
container.get<TokenDecoderInterface<WebSocketConnectionTokenData>>(TYPES.Auth_WebSocketConnectionTokenDecoder),
container.get<winston.Logger>(TYPES.Auth_Logger),
)
grpcServer.addService(SessionsService, {
validate: sessionsServer.validate.bind(sessionsServer),
grpcServer.addService(AuthService, {
validate: authServer.validate.bind(authServer),
validateWebsocket: authServer.validateWebsocket.bind(authServer),
})
grpcServer.bindAsync(`0.0.0.0:${gRPCPort}`, grpc.ServerCredentials.createInsecure(), (error, port) => {
if (error) {

View File

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

View File

@@ -310,7 +310,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: 'auth' },
defaultMeta: { service: `auth:${this.mode}` },
})
}
container.bind<winston.Logger>(TYPES.Auth_Logger).toConstantValue(logger)
@@ -1269,6 +1269,7 @@ export class ContainerConfigLoader {
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TriggerEmailBackupForUser>(TYPES.Auth_TriggerEmailBackupForUser),
container.get<GenerateRecoveryCodes>(TYPES.Auth_GenerateRecoveryCodes),
),
)
container

View File

@@ -159,6 +159,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
level: string
body: string
subject: string
userUuid?: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_REQUESTED',

View File

@@ -31,6 +31,7 @@ export interface DomainEventFactoryInterface {
level: string
body: string
subject: string
userUuid?: string
}): EmailRequestedEvent
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent
createUserRegisteredEvent(dto: {

View File

@@ -19,7 +19,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not find user with uuid: ${event.payload.userUuid}`)
this.logger.warn('Could not find user.', { userId: event.payload.userUuid })
return
}
@@ -28,7 +28,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
this.logger.warn('Could not find user.', { userId: userUuid.value })
return
}
@@ -37,7 +37,9 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
await this.userRepository.remove(user)
this.logger.info(`Finished account cleanup for user: ${userUuid.value}`)
this.logger.info('Finished account cleanup.', {
userId: userUuid.value,
})
}
private async removeSessions(userUuid: string): Promise<void> {

View File

@@ -15,7 +15,9 @@ export class EmailSubscriptionUnsubscribedEventHandler implements DomainEventHan
})
if (result.isFailed()) {
this.logger.error(`Failed to disable email setting for user ${event.payload.userEmail}: ${result.getError()}`)
this.logger.error(`Failed to disable email setting for user: ${result.getError()}`, {
userId: event.payload.userEmail,
})
}
}
}

View File

@@ -35,6 +35,7 @@ export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerI
subject: getSubject(),
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
userEmail: user.email,
userUuid: user.uuid,
}),
)
}

View File

@@ -131,6 +131,7 @@ export class SignIn implements UseCaseInterface {
),
messageIdentifier: 'SIGN_IN',
subject: getSubject(user.email),
userUuid: user.uuid,
}),
)
} catch (error) {

View File

@@ -39,7 +39,9 @@ export class TriggerEmailBackupForAllUsers implements UseCaseInterface<void> {
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`Failed to trigger email backup for user ${setting.props.userUuid.value}`)
this.logger.error(`Failed to trigger email backup for user: ${result.getError()}`, {
userId: setting.props.userUuid.value,
})
failedUsers++
}
}

View File

@@ -7,6 +7,7 @@ import {
import { EmailBackupFrequency, LogSessionUserAgentOption, MuteMarketingEmailsOption } from '@standardnotes/settings'
import { SettingName, Result } from '@standardnotes/domain-core'
import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes'
import { TriggerPostSettingUpdateActions } from './TriggerPostSettingUpdateActions'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
@@ -15,11 +16,20 @@ describe('TriggerPostSettingUpdateActions', () => {
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let triggerEmailBackupForUser: TriggerEmailBackupForUser
let generateRecoveryCodes: GenerateRecoveryCodes
const createUseCase = () =>
new TriggerPostSettingUpdateActions(domainEventPublisher, domainEventFactory, triggerEmailBackupForUser)
new TriggerPostSettingUpdateActions(
domainEventPublisher,
domainEventFactory,
triggerEmailBackupForUser,
generateRecoveryCodes,
)
beforeEach(() => {
generateRecoveryCodes = {} as jest.Mocked<GenerateRecoveryCodes>
generateRecoveryCodes.execute = jest.fn().mockReturnValue(Result.ok())
triggerEmailBackupForUser = {} as jest.Mocked<TriggerEmailBackupForUser>
triggerEmailBackupForUser.execute = jest.fn().mockReturnValue(Result.ok())
@@ -101,4 +111,15 @@ describe('TriggerPostSettingUpdateActions', () => {
username: 'test@test.te',
})
})
it('should generate new recovery codes upon enabling mfa setting', async () => {
await createUseCase().execute({
updatedSettingName: SettingName.NAMES.MfaSecret,
userUuid: '4-5-6',
userEmail: 'test@test.te',
unencryptedValue: '123',
})
expect(generateRecoveryCodes.execute).toHaveBeenCalled()
})
})

View File

@@ -5,6 +5,7 @@ import { EmailBackupFrequency, LogSessionUserAgentOption } from '@standardnotes/
import { TriggerPostSettingUpdateActionsDTO } from './TriggerPostSettingUpdateActionsDTO'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { TriggerEmailBackupForUser } from '../TriggerEmailBackupForUser/TriggerEmailBackupForUser'
import { GenerateRecoveryCodes } from '../GenerateRecoveryCodes/GenerateRecoveryCodes'
export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<string, string> = new Map([
@@ -18,6 +19,7 @@ export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private triggerEmailBackupForUser: TriggerEmailBackupForUser,
private generateRecoveryCodes: GenerateRecoveryCodes,
) {}
async execute(dto: TriggerPostSettingUpdateActionsDTO): Promise<Result<void>> {
@@ -35,6 +37,12 @@ export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
await this.triggerSessionUserAgentCleanup(dto.userEmail, dto.userUuid)
}
if (this.isEnablingMFASetting(dto.updatedSettingName, dto.unencryptedValue)) {
await this.generateRecoveryCodes.execute({
userUuid: dto.userUuid,
})
}
return Result.ok()
}
@@ -54,6 +62,10 @@ export class TriggerPostSettingUpdateActions implements UseCaseInterface<void> {
)
}
private isEnablingMFASetting(settingName: string, newValue: string | null): boolean {
return settingName === SettingName.NAMES.MfaSecret && newValue !== null
}
private isDisablingSessionUserAgentLogging(settingName: string, newValue: string | null): boolean {
return SettingName.NAMES.LogSessionUserAgent === settingName && LogSessionUserAgentOption.Disabled === newValue
}

View File

@@ -22,7 +22,7 @@ export class WebSocketsClientService implements ClientServiceInterface {
(await user.roles).map((role) => role.name),
)
this.logger.info(`[WebSockets] Requesting message ${event.type} to user ${user.uuid}`)
this.logger.debug(`[WebSockets] Requesting message ${event.type} to user ${user.uuid}`)
await this.domainEventPublisher.publish(
this.domainEventFactory.createWebSocketMessageRequestedEvent({

View File

@@ -1,20 +1,92 @@
import * as grpc from '@grpc/grpc-js'
import { Status } from '@grpc/grpc-js/build/src/constants'
import { AuthorizationHeader, ISessionsServer, SessionValidationResponse } from '@standardnotes/grpc'
import {
AuthorizationHeader,
ConnectionValidationResponse,
IAuthServer,
SessionValidationResponse,
WebsocketConnectionAuthorizationHeader,
} from '@standardnotes/grpc'
import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
import { User } from '../../Domain/User/User'
import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { Logger } from 'winston'
import { ErrorTag } from '@standardnotes/responses'
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
export class SessionsServer implements ISessionsServer {
export class AuthServer implements IAuthServer {
constructor(
private authenticateRequest: AuthenticateRequest,
private createCrossServiceToken: CreateCrossServiceToken,
protected tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
private logger: Logger,
) {}
async validateWebsocket(
call: grpc.ServerUnaryCall<WebsocketConnectionAuthorizationHeader, ConnectionValidationResponse>,
callback: grpc.sendUnaryData<ConnectionValidationResponse>,
): Promise<void> {
try {
const token: WebSocketConnectionTokenData | undefined = this.tokenDecoder.decodeToken(call.request.getToken())
if (token === undefined) {
const metadata = new grpc.Metadata()
metadata.set('x-auth-error-message', 'Invalid authorization token.')
metadata.set('x-auth-error-tag', ErrorTag.AuthInvalid)
metadata.set('x-auth-error-response-code', '401')
return callback(
{
code: Status.PERMISSION_DENIED,
message: 'Invalid authorization token.',
name: ErrorTag.AuthInvalid,
metadata,
},
null,
)
}
const resultOrError = await this.createCrossServiceToken.execute({
userUuid: token.userUuid,
sessionUuid: token.sessionUuid,
})
if (resultOrError.isFailed()) {
const metadata = new grpc.Metadata()
metadata.set('x-auth-error-message', resultOrError.getError())
metadata.set('x-auth-error-response-code', '400')
return callback(
{
code: Status.INVALID_ARGUMENT,
message: resultOrError.getError(),
name: 'INVALID_ARGUMENT',
metadata,
},
null,
)
}
const response = new ConnectionValidationResponse()
response.setCrossServiceToken(resultOrError.getValue())
this.logger.debug('[SessionsServer] Websocket connection validated via gRPC')
callback(null, response)
} catch (error) {
this.logger.error(`[SessionsServer] Error validating websocket connection via gRPC: ${(error as Error).message}`)
callback(
{
code: Status.UNKNOWN,
message: 'An error occurred while validating websocket connection',
name: 'UNKNOWN',
},
null,
)
}
}
async validate(
call: grpc.ServerUnaryCall<AuthorizationHeader, SessionValidationResponse>,
callback: grpc.sendUnaryData<SessionValidationResponse>,

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.22.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.4...@standardnotes/domain-events-infra@1.22.5) (2023-12-11)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.22.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.3...@standardnotes/domain-events-infra@1.22.4) (2023-12-01)
### Bug Fixes

View File

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

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.
## [2.138.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.138.1...@standardnotes/domain-events@2.138.2) (2023-12-11)
### Bug Fixes
* **syncing-server:** add user uuid for the emails requesting backup ([7b0ea0a](https://github.com/standardnotes/server/commit/7b0ea0a06975902e01951b13c84e941827dedd84))
## [2.138.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.138.0...@standardnotes/domain-events@2.138.1) (2023-11-28)
**Note:** Version bump only for package @standardnotes/domain-events

View File

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

View File

@@ -12,4 +12,5 @@ export interface EmailRequestedEventPayload {
attachmentFileName: string
attachmentContentType: string
}>
userUuid?: string
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.6](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.5...@standardnotes/files-server@1.36.6) (2023-12-11)
**Note:** Version bump only for package @standardnotes/files-server
## [1.36.5](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.4...@standardnotes/files-server@1.36.5) (2023-12-07)
### Bug Fixes
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
## [1.36.4](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.3...@standardnotes/files-server@1.36.4) (2023-12-01)
### Bug Fixes

View File

@@ -19,7 +19,7 @@ import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('server')
void container.load().then((container) => {
const env: Env = new Env()
env.load()

View File

@@ -9,7 +9,7 @@ import { DomainEventSubscriberInterface } from '@standardnotes/domain-events'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
const container = new ContainerConfigLoader()
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
dayjs.extend(utc)

View File

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

View File

@@ -54,6 +54,8 @@ import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
import { SharedVaultValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/SharedVaultValetTokenAuthMiddleware'
export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {}
async load(configuration?: {
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
logger?: Transform
@@ -316,7 +318,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(winston.format.splat(), winston.format.json()),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: 'files' },
defaultMeta: { service: `files:${this.mode}` },
})
}
}

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.4.0](https://github.com/standardnotes/server/compare/@standardnotes/grpc@1.3.2...@standardnotes/grpc@1.4.0) (2023-12-07)
### Features
* replace websocket connection validation with grpc ([#954](https://github.com/standardnotes/server/issues/954)) ([d5c1b76](https://github.com/standardnotes/server/commit/d5c1b76de068a64b334c4347cbefa973447a0f60))
## [1.3.2](https://github.com/standardnotes/server/compare/@standardnotes/grpc@1.3.1...@standardnotes/grpc@1.3.2) (2023-12-07)
**Note:** Version bump only for package @standardnotes/grpc

View File

@@ -7,12 +7,13 @@
import * as grpc from "@grpc/grpc-js";
import * as auth_pb from "./auth_pb";
interface ISessionsService extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
validate: ISessionsService_Ivalidate;
interface IAuthService extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
validate: IAuthService_Ivalidate;
validateWebsocket: IAuthService_IvalidateWebsocket;
}
interface ISessionsService_Ivalidate extends grpc.MethodDefinition<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse> {
path: "/auth.Sessions/validate";
interface IAuthService_Ivalidate extends grpc.MethodDefinition<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse> {
path: "/auth.Auth/validate";
requestStream: false;
responseStream: false;
requestSerialize: grpc.serialize<auth_pb.AuthorizationHeader>;
@@ -20,22 +21,38 @@ interface ISessionsService_Ivalidate extends grpc.MethodDefinition<auth_pb.Autho
responseSerialize: grpc.serialize<auth_pb.SessionValidationResponse>;
responseDeserialize: grpc.deserialize<auth_pb.SessionValidationResponse>;
}
export const SessionsService: ISessionsService;
export interface ISessionsServer {
validate: grpc.handleUnaryCall<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse>;
interface IAuthService_IvalidateWebsocket extends grpc.MethodDefinition<auth_pb.WebsocketConnectionAuthorizationHeader, auth_pb.ConnectionValidationResponse> {
path: "/auth.Auth/validateWebsocket";
requestStream: false;
responseStream: false;
requestSerialize: grpc.serialize<auth_pb.WebsocketConnectionAuthorizationHeader>;
requestDeserialize: grpc.deserialize<auth_pb.WebsocketConnectionAuthorizationHeader>;
responseSerialize: grpc.serialize<auth_pb.ConnectionValidationResponse>;
responseDeserialize: grpc.deserialize<auth_pb.ConnectionValidationResponse>;
}
export interface ISessionsClient {
export const AuthService: IAuthService;
export interface IAuthServer {
validate: grpc.handleUnaryCall<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse>;
validateWebsocket: grpc.handleUnaryCall<auth_pb.WebsocketConnectionAuthorizationHeader, auth_pb.ConnectionValidationResponse>;
}
export interface IAuthClient {
validate(request: auth_pb.AuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
}
export class SessionsClient extends grpc.Client implements ISessionsClient {
export class AuthClient extends grpc.Client implements IAuthClient {
constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
public validate(request: auth_pb.AuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
public validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
public validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
public validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
public validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
public validateWebsocket(request: auth_pb.WebsocketConnectionAuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.ConnectionValidationResponse) => void): grpc.ClientUnaryCall;
}

View File

@@ -15,6 +15,17 @@ function deserialize_auth_AuthorizationHeader(buffer_arg) {
return auth_pb.AuthorizationHeader.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_auth_ConnectionValidationResponse(arg) {
if (!(arg instanceof auth_pb.ConnectionValidationResponse)) {
throw new Error('Expected argument of type auth.ConnectionValidationResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_auth_ConnectionValidationResponse(buffer_arg) {
return auth_pb.ConnectionValidationResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_auth_SessionValidationResponse(arg) {
if (!(arg instanceof auth_pb.SessionValidationResponse)) {
throw new Error('Expected argument of type auth.SessionValidationResponse');
@@ -26,10 +37,21 @@ function deserialize_auth_SessionValidationResponse(buffer_arg) {
return auth_pb.SessionValidationResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_auth_WebsocketConnectionAuthorizationHeader(arg) {
if (!(arg instanceof auth_pb.WebsocketConnectionAuthorizationHeader)) {
throw new Error('Expected argument of type auth.WebsocketConnectionAuthorizationHeader');
}
return Buffer.from(arg.serializeBinary());
}
var SessionsService = exports.SessionsService = {
function deserialize_auth_WebsocketConnectionAuthorizationHeader(buffer_arg) {
return auth_pb.WebsocketConnectionAuthorizationHeader.deserializeBinary(new Uint8Array(buffer_arg));
}
var AuthService = exports.AuthService = {
validate: {
path: '/auth.Sessions/validate',
path: '/auth.Auth/validate',
requestStream: false,
responseStream: false,
requestType: auth_pb.AuthorizationHeader,
@@ -39,6 +61,17 @@ var SessionsService = exports.SessionsService = {
responseSerialize: serialize_auth_SessionValidationResponse,
responseDeserialize: deserialize_auth_SessionValidationResponse,
},
validateWebsocket: {
path: '/auth.Auth/validateWebsocket',
requestStream: false,
responseStream: false,
requestType: auth_pb.WebsocketConnectionAuthorizationHeader,
responseType: auth_pb.ConnectionValidationResponse,
requestSerialize: serialize_auth_WebsocketConnectionAuthorizationHeader,
requestDeserialize: deserialize_auth_WebsocketConnectionAuthorizationHeader,
responseSerialize: serialize_auth_ConnectionValidationResponse,
responseDeserialize: deserialize_auth_ConnectionValidationResponse,
},
};
exports.SessionsClient = grpc.makeGenericClientConstructor(SessionsService);
exports.AuthClient = grpc.makeGenericClientConstructor(AuthService);

View File

@@ -45,3 +45,43 @@ export namespace SessionValidationResponse {
crossServiceToken: string,
}
}
export class WebsocketConnectionAuthorizationHeader extends jspb.Message {
getToken(): string;
setToken(value: string): WebsocketConnectionAuthorizationHeader;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): WebsocketConnectionAuthorizationHeader.AsObject;
static toObject(includeInstance: boolean, msg: WebsocketConnectionAuthorizationHeader): WebsocketConnectionAuthorizationHeader.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: WebsocketConnectionAuthorizationHeader, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): WebsocketConnectionAuthorizationHeader;
static deserializeBinaryFromReader(message: WebsocketConnectionAuthorizationHeader, reader: jspb.BinaryReader): WebsocketConnectionAuthorizationHeader;
}
export namespace WebsocketConnectionAuthorizationHeader {
export type AsObject = {
token: string,
}
}
export class ConnectionValidationResponse extends jspb.Message {
getCrossServiceToken(): string;
setCrossServiceToken(value: string): ConnectionValidationResponse;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ConnectionValidationResponse.AsObject;
static toObject(includeInstance: boolean, msg: ConnectionValidationResponse): ConnectionValidationResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: ConnectionValidationResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): ConnectionValidationResponse;
static deserializeBinaryFromReader(message: ConnectionValidationResponse, reader: jspb.BinaryReader): ConnectionValidationResponse;
}
export namespace ConnectionValidationResponse {
export type AsObject = {
crossServiceToken: string,
}
}

View File

@@ -22,7 +22,9 @@ var global = (function() {
}.call(null));
goog.exportSymbol('proto.auth.AuthorizationHeader', null, global);
goog.exportSymbol('proto.auth.ConnectionValidationResponse', null, global);
goog.exportSymbol('proto.auth.SessionValidationResponse', null, global);
goog.exportSymbol('proto.auth.WebsocketConnectionAuthorizationHeader', null, global);
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -65,6 +67,48 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.auth.SessionValidationResponse.displayName = 'proto.auth.SessionValidationResponse';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.auth.WebsocketConnectionAuthorizationHeader = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.auth.WebsocketConnectionAuthorizationHeader, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.auth.WebsocketConnectionAuthorizationHeader.displayName = 'proto.auth.WebsocketConnectionAuthorizationHeader';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.auth.ConnectionValidationResponse = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.auth.ConnectionValidationResponse, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.auth.ConnectionValidationResponse.displayName = 'proto.auth.ConnectionValidationResponse';
}
@@ -325,4 +369,264 @@ proto.auth.SessionValidationResponse.prototype.setCrossServiceToken = function(v
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.auth.WebsocketConnectionAuthorizationHeader.prototype.toObject = function(opt_includeInstance) {
return proto.auth.WebsocketConnectionAuthorizationHeader.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.auth.WebsocketConnectionAuthorizationHeader} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.auth.WebsocketConnectionAuthorizationHeader.toObject = function(includeInstance, msg) {
var f, obj = {
token: jspb.Message.getFieldWithDefault(msg, 1, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.auth.WebsocketConnectionAuthorizationHeader}
*/
proto.auth.WebsocketConnectionAuthorizationHeader.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.auth.WebsocketConnectionAuthorizationHeader;
return proto.auth.WebsocketConnectionAuthorizationHeader.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.auth.WebsocketConnectionAuthorizationHeader} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.auth.WebsocketConnectionAuthorizationHeader}
*/
proto.auth.WebsocketConnectionAuthorizationHeader.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setToken(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.auth.WebsocketConnectionAuthorizationHeader.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.auth.WebsocketConnectionAuthorizationHeader.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.auth.WebsocketConnectionAuthorizationHeader} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.auth.WebsocketConnectionAuthorizationHeader.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getToken();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
};
/**
* optional string token = 1;
* @return {string}
*/
proto.auth.WebsocketConnectionAuthorizationHeader.prototype.getToken = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.auth.WebsocketConnectionAuthorizationHeader} returns this
*/
proto.auth.WebsocketConnectionAuthorizationHeader.prototype.setToken = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.auth.ConnectionValidationResponse.prototype.toObject = function(opt_includeInstance) {
return proto.auth.ConnectionValidationResponse.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.auth.ConnectionValidationResponse} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.auth.ConnectionValidationResponse.toObject = function(includeInstance, msg) {
var f, obj = {
crossServiceToken: jspb.Message.getFieldWithDefault(msg, 1, "")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.auth.ConnectionValidationResponse}
*/
proto.auth.ConnectionValidationResponse.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.auth.ConnectionValidationResponse;
return proto.auth.ConnectionValidationResponse.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.auth.ConnectionValidationResponse} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.auth.ConnectionValidationResponse}
*/
proto.auth.ConnectionValidationResponse.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setCrossServiceToken(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.auth.ConnectionValidationResponse.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.auth.ConnectionValidationResponse.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.auth.ConnectionValidationResponse} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.auth.ConnectionValidationResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getCrossServiceToken();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
};
/**
* optional string cross_service_token = 1;
* @return {string}
*/
proto.auth.ConnectionValidationResponse.prototype.getCrossServiceToken = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.auth.ConnectionValidationResponse} returns this
*/
proto.auth.ConnectionValidationResponse.prototype.setCrossServiceToken = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
goog.object.extend(exports, proto.auth);

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/grpc",
"version": "1.3.2",
"version": "1.4.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -10,6 +10,15 @@ message SessionValidationResponse {
string cross_service_token = 1;
}
service Sessions {
rpc validate(AuthorizationHeader) returns (SessionValidationResponse) {}
message WebsocketConnectionAuthorizationHeader {
string token = 1;
}
message ConnectionValidationResponse {
string cross_service_token = 1;
}
service Auth {
rpc validate(AuthorizationHeader) returns (SessionValidationResponse) {}
rpc validateWebsocket(WebsocketConnectionAuthorizationHeader) returns (ConnectionValidationResponse) {}
}

View File

@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.20](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.19...@standardnotes/home-server@1.22.20) (2023-12-12)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.19](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.18...@standardnotes/home-server@1.22.19) (2023-12-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.18](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.17...@standardnotes/home-server@1.22.18) (2023-12-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.17](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.16...@standardnotes/home-server@1.22.17) (2023-12-11)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.16](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.15...@standardnotes/home-server@1.22.16) (2023-12-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.15](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.14...@standardnotes/home-server@1.22.15) (2023-12-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.14](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.13...@standardnotes/home-server@1.22.14) (2023-12-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.13](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.12...@standardnotes/home-server@1.22.13) (2023-12-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.12](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.11...@standardnotes/home-server@1.22.12) (2023-12-07)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.11](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.10...@standardnotes/home-server@1.22.11) (2023-12-07)
**Note:** Version bump only for package @standardnotes/home-server

View File

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

View File

@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.51.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.5...@standardnotes/revisions-server@1.51.6) (2023-12-11)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.51.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.4...@standardnotes/revisions-server@1.51.5) (2023-12-07)
### Bug Fixes
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
* logs meta ([8ac84c5](https://github.com/standardnotes/server/commit/8ac84c59af886bb9c42de012fb1e7864e116ab55))
## [1.51.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.3...@standardnotes/revisions-server@1.51.4) (2023-12-01)
### Bug Fixes

View File

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

View File

@@ -90,7 +90,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: 'revisions' },
defaultMeta: { service: `revisions:${this.mode}` },
})
}
container.bind<winston.Logger>(TYPES.Revisions_Logger).toConstantValue(logger)

View File

@@ -12,7 +12,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Failed account cleanup: ${userUuidOrError.getError()}`)
this.logger.warn(`Failed account cleanup: ${userUuidOrError.getError()}`, { userId: event.payload.userUuid })
return
}
@@ -20,6 +20,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
await this.revisionRepository.removeByUserUuid(userUuid)
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
this.logger.info('Finished account cleanup.', { userId: event.payload.userUuid })
}
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.27.11](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.10...@standardnotes/scheduler-server@1.27.11) (2023-12-11)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.27.10](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.9...@standardnotes/scheduler-server@1.27.10) (2023-12-07)
### Bug Fixes
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
## [1.27.9](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.8...@standardnotes/scheduler-server@1.27.9) (2023-12-01)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.27.9",
"version": "1.27.11",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -62,6 +62,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: 'scheduler' },
})
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.129.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.128.2...@standardnotes/syncing-server@1.129.0) (2023-12-12)
### Features
* **syncing-server:** add extended revisions frequency for free users ([#965](https://github.com/standardnotes/server/issues/965)) ([398c10c](https://github.com/standardnotes/server/commit/398c10ce4b8e357728a8b4f354b3bf6ccc8e438d))
## [1.128.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.128.1...@standardnotes/syncing-server@1.128.2) (2023-12-11)
### Bug Fixes
* **syncing-server:** add user uuid for the emails requesting backup ([7b0ea0a](https://github.com/standardnotes/server/commit/7b0ea0a06975902e01951b13c84e941827dedd84))
## [1.128.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.128.0...@standardnotes/syncing-server@1.128.1) (2023-12-11)
### Bug Fixes
* **syncing-server:** logs meta for email with backup requested ([597ff13](https://github.com/standardnotes/server/commit/597ff13393965a6d6f3a35e12d41d648543d35b7))
# [1.128.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.9...@standardnotes/syncing-server@1.128.0) (2023-12-08)
### Features
* **syncing-server:** send websocket event to shared vault members upon items change in shared vault ([#961](https://github.com/standardnotes/server/issues/961)) ([6dbb877](https://github.com/standardnotes/server/commit/6dbb87708faf6c6f4ec28b45570390b6c816a7a2))
## [1.127.9](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.8...@standardnotes/syncing-server@1.127.9) (2023-12-07)
### Bug Fixes
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
* logs meta ([8ac84c5](https://github.com/standardnotes/server/commit/8ac84c59af886bb9c42de012fb1e7864e116ab55))
## [1.127.8](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.7...@standardnotes/syncing-server@1.127.8) (2023-12-07)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.127.7](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.6...@standardnotes/syncing-server@1.127.7) (2023-12-07)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.127.7",
"version": "1.129.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -161,6 +161,7 @@ import { SyncResponse20200115 } from '../Domain/Item/SyncResponse/SyncResponse20
import { SyncResponse } from '@standardnotes/grpc'
import { SyncResponseGRPCMapper } from '../Mapping/gRPC/SyncResponseGRPCMapper'
import { AccountDeletionVerificationRequestedEventHandler } from '../Domain/Handler/AccountDeletionVerificationRequestedEventHandler'
import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients/SendEventToClients'
export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -195,7 +196,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: 'syncing-server' },
defaultMeta: { service: `syncing-server:${this.mode}` },
})
}
container.bind<winston.Logger>(TYPES.Sync_Logger).toConstantValue(logger)
@@ -457,6 +458,9 @@ export class ContainerConfigLoader {
container
.bind(TYPES.Sync_REVISIONS_FREQUENCY)
.toConstantValue(env.get('REVISIONS_FREQUENCY', true) ? +env.get('REVISIONS_FREQUENCY', true) : 300)
container
.bind(TYPES.Sync_FREE_REVISIONS_FREQUENCY)
.toConstantValue(env.get('FREE_REVISIONS_FREQUENCY', true) ? +env.get('FREE_REVISIONS_FREQUENCY', true) : 86_400)
container.bind(TYPES.Sync_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
container
.bind(TYPES.Sync_CONTENT_SIZE_TRANSFER_LIMIT)
@@ -561,6 +565,15 @@ export class ContainerConfigLoader {
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<SendEventToClients>(TYPES.Sync_SendEventToClients)
.toConstantValue(
new SendEventToClients(
container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
.bind<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser)
.toConstantValue(
@@ -591,6 +604,7 @@ export class ContainerConfigLoader {
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<number>(TYPES.Sync_FREE_REVISIONS_FREQUENCY),
container.get<number>(TYPES.Sync_REVISIONS_FREQUENCY),
container.get<DetermineSharedVaultOperationOnItem>(TYPES.Sync_DetermineSharedVaultOperationOnItem),
container.get<AddNotificationsForUsers>(TYPES.Sync_AddNotificationsForUsers),
@@ -607,6 +621,7 @@ export class ContainerConfigLoader {
container.get<SaveNewItem>(TYPES.Sync_SaveNewItem),
container.get<UpdateExistingItem>(TYPES.Sync_UpdateExistingItem),
container.get<SendEventToClient>(TYPES.Sync_SendEventToClient),
container.get<SendEventToClients>(TYPES.Sync_SendEventToClients),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<Logger>(TYPES.Sync_Logger),
),

View File

@@ -34,6 +34,7 @@ const TYPES = {
Sync_S3_BACKUP_BUCKET_NAME: Symbol.for('Sync_S3_BACKUP_BUCKET_NAME'),
Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE: Symbol.for('Sync_EMAIL_ATTACHMENT_MAX_BYTE_SIZE'),
Sync_REVISIONS_FREQUENCY: Symbol.for('Sync_REVISIONS_FREQUENCY'),
Sync_FREE_REVISIONS_FREQUENCY: Symbol.for('Sync_FREE_REVISIONS_FREQUENCY'),
Sync_VERSION: Symbol.for('Sync_VERSION'),
Sync_CONTENT_SIZE_TRANSFER_LIMIT: Symbol.for('Sync_CONTENT_SIZE_TRANSFER_LIMIT'),
Sync_MAX_ITEMS_LIMIT: Symbol.for('Sync_MAX_ITEMS_LIMIT'),
@@ -77,6 +78,7 @@ const TYPES = {
Sync_UpdateStorageQuotaUsedInSharedVault: Symbol.for('Sync_UpdateStorageQuotaUsedInSharedVault'),
Sync_AddNotificationsForUsers: Symbol.for('Sync_AddNotificationsForUsers'),
Sync_SendEventToClient: Symbol.for('Sync_SendEventToClient'),
Sync_SendEventToClients: Symbol.for('Sync_SendEventToClients'),
Sync_RemoveItemsFromSharedVault: Symbol.for('Sync_RemoveItemsFromSharedVault'),
Sync_DesignateSurvivor: Symbol.for('Sync_DesignateSurvivor'),
Sync_RemoveUserFromSharedVaults: Symbol.for('Sync_RemoveUserFromSharedVaults'),

View File

@@ -329,6 +329,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
attachmentFileName: string
attachmentContentType: string
}>
userUuid?: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_REQUESTED',

View File

@@ -74,6 +74,7 @@ export interface DomainEventFactoryInterface {
attachmentFileName: string
attachmentContentType: string
}>
userUuid?: string
}): EmailRequestedEvent
createDuplicateItemSyncedEvent(dto: { itemUuid: string; userUuid: string }): DuplicateItemSyncedEvent
createItemRevisionCreationRequested(dto: { itemUuid: string; userUuid: string }): ItemRevisionCreationRequestedEvent

View File

@@ -17,7 +17,10 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.error(`AccountDeletionRequestedEventHandler failed: ${userUuidOrError.getError()}`)
this.logger.error(userUuidOrError.getError(), {
userId: event.payload.userUuid,
codeTag: 'AccountDeletionRequestedEventHandler',
})
return
}
@@ -30,9 +33,9 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
allowSurviving: true,
})
if (deletingVaultsResult.isFailed()) {
this.logger.error(
`Failed to delete shared vaults for user: ${event.payload.userUuid}: ${deletingVaultsResult.getError()}`,
)
this.logger.error(`Failed to delete shared vaults: ${deletingVaultsResult.getError()}`, {
userId: event.payload.userUuid,
})
}
const deletedSharedVaultUuids = Array.from(deletingVaultsResult.getValue().keys())
@@ -49,13 +52,13 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
userUuid: event.payload.userUuid,
})
if (deletingUserFromOtherVaultsResult.isFailed()) {
this.logger.error(
`Failed to remove user: ${
event.payload.userUuid
} from shared vaults: ${deletingUserFromOtherVaultsResult.getError()}`,
)
this.logger.error(`Failed to remove user from shared vaults: ${deletingUserFromOtherVaultsResult.getError()}`, {
userId: event.payload.userUuid,
})
}
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
this.logger.info('Finished account cleanup', {
userId: event.payload.userUuid,
})
}
}

View File

@@ -81,10 +81,13 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
attachmentContentType: 'application/json',
},
],
userUuid: event.payload.userUuid,
}),
)
}
this.logger.info(`Email with backup requested for user ${event.payload.userUuid}`)
this.logger.info('Email with backup requested for user', {
userId: event.payload.userUuid,
})
}
}

View File

@@ -11,6 +11,8 @@ import { Item } from '../../../Item/Item'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { ItemsChangedOnServerEvent } from '@standardnotes/domain-events'
import { SendEventToClients } from '../SendEventToClients/SendEventToClients'
import { SharedVaultAssociation } from '../../../SharedVault/SharedVaultAssociation'
describe('SaveItems', () => {
let itemSaveValidator: ItemSaveValidatorInterface
@@ -22,6 +24,7 @@ describe('SaveItems', () => {
let itemHash1: ItemHash
let savedItem: Item
let sendEventToClient: SendEventToClient
let sendEventToClients: SendEventToClients
let domainEventFactory: DomainEventFactoryInterface
const createUseCase = () =>
@@ -32,6 +35,7 @@ describe('SaveItems', () => {
saveNewItem,
updateExistingItem,
sendEventToClient,
sendEventToClients,
domainEventFactory,
logger,
)
@@ -40,6 +44,9 @@ describe('SaveItems', () => {
sendEventToClient = {} as jest.Mocked<SendEventToClient>
sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok())
sendEventToClients = {} as jest.Mocked<SendEventToClients>
sendEventToClients.execute = jest.fn().mockReturnValue(Result.ok())
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createItemsChangedOnServerEvent = jest
.fn()
@@ -106,6 +113,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -130,6 +138,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -154,6 +163,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -175,6 +185,7 @@ describe('SaveItems', () => {
readOnlyAccess: true,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -197,6 +208,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -215,6 +227,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -233,16 +246,65 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(updateExistingItem.execute).toHaveBeenCalledWith({
isFreeUser: false,
itemHash: itemHash1,
existingItem: savedItem,
sessionUuid: 'session-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(sendEventToClient.execute).toHaveBeenCalled()
expect(sendEventToClients.execute).not.toHaveBeenCalled()
})
it('should update existing shared vault items', async () => {
savedItem = Item.create({
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
sharedVaultAssociation: SharedVaultAssociation.create({
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
}).getValue(),
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
}).getValue()
const useCase = createUseCase()
itemRepository.findByUuid = jest.fn().mockResolvedValue(savedItem)
updateExistingItem.execute = jest.fn().mockResolvedValue(Result.ok(savedItem))
const result = await useCase.execute({
itemHashes: [itemHash1],
userUuid: '00000000-0000-0000-0000-000000000000',
apiVersion: '1',
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(updateExistingItem.execute).toHaveBeenCalledWith({
isFreeUser: false,
itemHash: itemHash1,
existingItem: savedItem,
sessionUuid: 'session-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
})
expect(sendEventToClient.execute).toHaveBeenCalled()
expect(sendEventToClients.execute).toHaveBeenCalled()
})
it('should mark items as conflicts if updating existing item fails', async () => {
@@ -258,6 +320,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -282,6 +345,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -327,6 +391,7 @@ describe('SaveItems', () => {
readOnlyAccess: false,
sessionUuid: 'session-uuid',
snjsVersion: '2.200.0',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()

View File

@@ -13,6 +13,7 @@ import { UpdateExistingItem } from '../UpdateExistingItem/UpdateExistingItem'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SendEventToClients } from '../SendEventToClients/SendEventToClients'
export class SaveItems implements UseCaseInterface<SaveItemsResult> {
private readonly SYNC_TOKEN_VERSION = 2
@@ -24,6 +25,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
private saveNewItem: SaveNewItem,
private updateExistingItem: UpdateExistingItem,
private sendEventToClient: SendEventToClient,
private sendEventToClients: SendEventToClients,
private domainEventFactory: DomainEventFactoryInterface,
private logger: Logger,
) {}
@@ -82,6 +84,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
itemHash,
sessionUuid: dto.sessionUuid,
performingUserUuid: dto.userUuid,
isFreeUser: dto.isFreeUser,
})
if (udpatedItemOrError.isFailed()) {
this.logger.error(
@@ -167,7 +170,31 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`[${dto.userUuid}] Sending items changed event to client failed. Error: ${result.getError()}`)
this.logger.error(`Sending items changed event to client failed. Error: ${result.getError()}`, {
userId: dto.userUuid,
})
}
const sharedVaultUuidsMap = new Map<string, boolean>()
for (const item of savedItems) {
if (item.isAssociatedWithASharedVault()) {
sharedVaultUuidsMap.set((item.sharedVaultUuid as Uuid).value, true)
}
}
const sharedVaultUuids = Array.from(sharedVaultUuidsMap.keys())
for (const sharedVaultUuid of sharedVaultUuids) {
const result = await this.sendEventToClients.execute({
sharedVaultUuid,
event: itemsChangedEvent,
originatingUserUuid: dto.userUuid,
})
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`Sending items changed event to clients failed. Error: ${result.getError()}`, {
userId: dto.userUuid,
sharedVaultUuid,
})
}
}
}

View File

@@ -7,4 +7,5 @@ export interface SaveItemsDTO {
readOnlyAccess: boolean
sessionUuid: string | null
snjsVersion: string
isFreeUser: boolean
}

View File

@@ -0,0 +1,108 @@
import { Logger } from 'winston'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { SendEventToClients } from './SendEventToClients'
import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
import { DomainEventInterface } from '@standardnotes/domain-events'
describe('SendEventToClients', () => {
let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
let sendEventToClient: SendEventToClient
let logger: Logger
const createUseCase = () => new SendEventToClients(sharedVaultUserRepository, sendEventToClient, logger)
beforeEach(() => {
const sharedVaultUser = SharedVaultUser.create({
permission: SharedVaultUserPermission.create('read').getValue(),
sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
timestamps: Timestamps.create(123456789, 123456789).getValue(),
userUuid: Uuid.create('00000000-0000-0000-0000-000000000001').getValue(),
isDesignatedSurvivor: false,
}).getValue()
sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
sharedVaultUserRepository.findBySharedVaultUuid = jest.fn().mockReturnValue([sharedVaultUser])
sendEventToClient = {} as jest.Mocked<SendEventToClient>
sendEventToClient.execute = jest.fn().mockReturnValue(Result.ok())
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
})
it('should send event to all users', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000003',
})
expect(result.isFailed()).toBeFalsy()
expect(sendEventToClient.execute).toHaveBeenCalledTimes(1)
})
it('should send event to all users except the originating one', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.isFailed()).toBeFalsy()
expect(sendEventToClient.execute).toHaveBeenCalledTimes(0)
})
it('should return error if shared vault uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: 'invalid',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000001',
})
expect(result.isFailed()).toBeTruthy()
})
it('should return error if originating user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: 'invalid',
})
expect(result.isFailed()).toBeTruthy()
})
it('should log error if sending event to client failed', async () => {
sendEventToClient.execute = jest.fn().mockReturnValue(Result.fail('test error'))
const useCase = createUseCase()
const result = await useCase.execute({
sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
event: {
type: 'test',
} as jest.Mocked<DomainEventInterface>,
originatingUserUuid: '00000000-0000-0000-0000-000000000003',
})
expect(result.isFailed()).toBeFalsy()
expect(logger.error).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,50 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { SendEventToClientsDTO } from './SendEventToClientsDTO'
import { SendEventToClient } from '../SendEventToClient/SendEventToClient'
import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
export class SendEventToClients implements UseCaseInterface<void> {
constructor(
private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
private sendEventToClient: SendEventToClient,
private logger: Logger,
) {}
async execute(dto: SendEventToClientsDTO): Promise<Result<void>> {
const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
if (sharedVaultUuidOrError.isFailed()) {
return Result.fail(sharedVaultUuidOrError.getError())
}
const sharedVaultUuid = sharedVaultUuidOrError.getValue()
const originatingUserUuidOrError = Uuid.create(dto.originatingUserUuid)
if (originatingUserUuidOrError.isFailed()) {
return Result.fail(originatingUserUuidOrError.getError())
}
const originatingUserUuid = originatingUserUuidOrError.getValue()
const sharedVaultUsers = await this.sharedVaultUserRepository.findBySharedVaultUuid(sharedVaultUuid)
for (const sharedVaultUser of sharedVaultUsers) {
if (originatingUserUuid.equals(sharedVaultUser.props.userUuid)) {
continue
}
const result = await this.sendEventToClient.execute({
event: dto.event,
userUuid: sharedVaultUser.props.userUuid.value,
})
if (result.isFailed()) {
this.logger.error(`Failed to send event to client: ${result.getError()}`, {
userId: sharedVaultUser.props.userUuid.value,
sharedVaultUuid: sharedVaultUuid.value,
})
}
}
return Result.ok()
}
}

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from '@standardnotes/domain-events'
export interface SendEventToClientsDTO {
sharedVaultUuid: string
event: DomainEventInterface
originatingUserUuid: string
}

View File

@@ -156,6 +156,7 @@ describe('SyncItems', () => {
apiVersion: ApiVersion.v20200115,
sessionUuid: null,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.getValue()).toEqual({
conflicts: [],
@@ -181,6 +182,7 @@ describe('SyncItems', () => {
userUuid: '1-2-3',
apiVersion: '20200115',
snjsVersion: '1.2.3',
isFreeUser: false,
readOnlyAccess: false,
sessionUuid: null,
})
@@ -205,6 +207,7 @@ describe('SyncItems', () => {
apiVersion: ApiVersion.v20200115,
sessionUuid: null,
snjsVersion: '1.2.3',
isFreeUser: false,
})
} catch (error) {
caughtError = error
@@ -224,6 +227,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.getValue()).toEqual({
conflicts: [],
@@ -249,6 +253,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
sharedVaultUuids: ['00000000-0000-0000-0000-000000000000'],
})
expect(result.getValue()).toEqual({
@@ -301,6 +306,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.getValue()).toEqual({
@@ -340,6 +346,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -360,6 +367,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -380,6 +388,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -400,6 +409,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -420,6 +430,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -440,6 +451,7 @@ describe('SyncItems', () => {
contentType: 'Note',
apiVersion: ApiVersion.v20200115,
snjsVersion: '1.2.3',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()

View File

@@ -47,6 +47,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
readOnlyAccess: dto.readOnlyAccess,
sessionUuid: dto.sessionUuid,
snjsVersion: dto.snjsVersion,
isFreeUser: dto.isFreeUser,
})
if (saveItemsResultOrError.isFailed()) {
return Result.fail(saveItemsResultOrError.getError())

View File

@@ -13,4 +13,5 @@ export type SyncItemsDTO = {
snjsVersion: string
readOnlyAccess: boolean
sessionUuid: string | null
isFreeUser: boolean
}

View File

@@ -38,6 +38,7 @@ describe('UpdateExistingItem', () => {
timer,
domainEventPublisher,
domainEventFactory,
86_400,
5,
determineSharedVaultOperationOnItem,
addNotificationsForUsers,
@@ -137,6 +138,7 @@ describe('UpdateExistingItem', () => {
itemHash: itemHash1,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -151,6 +153,7 @@ describe('UpdateExistingItem', () => {
itemHash: itemHash1,
sessionUuid: 'invalid-uuid',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -167,6 +170,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -183,6 +187,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -206,6 +211,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -224,6 +230,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -241,6 +248,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -260,6 +268,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -280,6 +289,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -300,6 +310,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -323,6 +334,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -347,6 +359,7 @@ describe('UpdateExistingItem', () => {
}).getValue(),
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -361,6 +374,7 @@ describe('UpdateExistingItem', () => {
itemHash: itemHash1,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: 'invalid-uuid',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -379,6 +393,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.sharedVaultAssociation).not.toBeUndefined()
@@ -405,6 +420,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -442,6 +458,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -470,6 +487,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()
@@ -490,6 +508,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -521,6 +540,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -555,6 +575,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
@@ -576,6 +597,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -595,6 +617,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
expect(item1.props.keySystemAssociation).not.toBeUndefined()
@@ -616,6 +639,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeFalsy()
@@ -636,6 +660,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
})
@@ -658,6 +683,7 @@ describe('UpdateExistingItem', () => {
itemHash,
sessionUuid: '00000000-0000-0000-0000-000000000000',
performingUserUuid: '00000000-0000-0000-0000-000000000000',
isFreeUser: false,
})
expect(result.isFailed()).toBeTruthy()
mock.mockRestore()

View File

@@ -31,7 +31,8 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
private timer: TimerInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private revisionFrequency: number,
private freeRevisionFrequency: number,
private premiumRevisionFrequency: number,
private determineSharedVaultOperationOnItem: DetermineSharedVaultOperationOnItem,
private addNotificationForUsers: AddNotificationsForUsers,
private removeNotificationsForUser: RemoveNotificationsForUser,
@@ -169,7 +170,10 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
await this.itemRepository.update(dto.existingItem)
if (secondsFromLastUpdate >= this.revisionFrequency) {
/* istanbul ignore next */
const revisionsFrequency = dto.isFreeUser ? this.freeRevisionFrequency : this.premiumRevisionFrequency
if (secondsFromLastUpdate >= revisionsFrequency) {
if (
dto.existingItem.props.contentType.value !== null &&
[ContentType.TYPES.Note, ContentType.TYPES.File].includes(dto.existingItem.props.contentType.value)

View File

@@ -6,4 +6,5 @@ export interface UpdateExistingItemDTO {
itemHash: ItemHash
sessionUuid: string | null
performingUserUuid: string
isFreeUser: boolean
}

View File

@@ -70,6 +70,7 @@ export class BaseItemsController extends BaseHttpController {
readOnlyAccess: response.locals.readOnlyAccess,
sessionUuid: response.locals.session ? response.locals.session.uuid : null,
sharedVaultUuids,
isFreeUser: response.locals.isFreeUser,
})
if (syncResult.isFailed()) {
return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)

View File

@@ -3,6 +3,7 @@ import { BaseMiddleware } from 'inversify-express-utils'
import { verify } from 'jsonwebtoken'
import { CrossServiceTokenData } from '@standardnotes/security'
import * as winston from 'winston'
import { RoleName } from '@standardnotes/domain-core'
export class InversifyExpressAuthMiddleware extends BaseMiddleware {
constructor(
@@ -26,6 +27,8 @@ export class InversifyExpressAuthMiddleware extends BaseMiddleware {
response.locals.user = decodedToken.user
response.locals.roles = decodedToken.roles
response.locals.isFreeUser =
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
response.locals.session = decodedToken.session
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context

View File

@@ -88,6 +88,7 @@ export class SyncingServer implements ISyncingServer {
readOnlyAccess: call.metadata.get('x-read-only-access').pop() === 'true',
sessionUuid: call.metadata.get('x-session-uuid').pop() as string,
sharedVaultUuids,
isFreeUser: call.metadata.get('x-is-free-user').pop() === 'true',
})
if (syncResult.isFailed()) {
const metadata = new grpc.Metadata()

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.22.1...@standardnotes/websockets-server@1.22.2) (2023-12-11)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.22.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.22.0...@standardnotes/websockets-server@1.22.1) (2023-12-07)
### Bug Fixes
* logger meta information ([a2b1323](https://github.com/standardnotes/server/commit/a2b1323568f5ced74b41aa4634340a6ca0668683))
# [1.22.0](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.21.4...@standardnotes/websockets-server@1.22.0) (2023-12-07)
### Features
* replace websocket connection validation with grpc ([#954](https://github.com/standardnotes/server/issues/954)) ([d5c1b76](https://github.com/standardnotes/server/commit/d5c1b76de068a64b334c4347cbefa973447a0f60))
## [1.21.4](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.21.3...@standardnotes/websockets-server@1.21.4) (2023-12-07)
### Bug Fixes
* **websockets:** remove connection trace when it is gone ([#953](https://github.com/standardnotes/server/issues/953)) ([96a2a67](https://github.com/standardnotes/server/commit/96a2a67aa6baa70ddb2d69a9ccbf62df044605e5))
## [1.21.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.21.2...@standardnotes/websockets-server@1.21.3) (2023-12-06)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.21.3",
"version": "1.22.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -49,6 +49,7 @@ export class ContainerConfigLoader {
level: env.get('LOG_LEVEL', true) || 'info',
format: winston.format.combine(...winstonFormatters),
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL', true) || 'info' })],
defaultMeta: { service: `websockets:${this.mode}` },
})
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)

View File

@@ -16,7 +16,9 @@ export class WebSocketMessageRequestedEventHandler implements DomainEventHandler
})
if (result.isFailed()) {
this.logger.error(`Could not send message to user ${event.payload.userUuid}. Error: ${result.getError()}`)
this.logger.error(`Could not send message to user. Error: ${result.getError()}`, {
userId: event.payload.userUuid,
})
}
}
}

View File

@@ -48,9 +48,10 @@ export class AddWebSocketsConnection implements UseCaseInterface<void> {
return Result.ok()
} catch (error) {
this.logger.error(
`Error persisting connection ${dto.connectionId} for user ${dto.userUuid}: ${(error as Error).message}`,
)
this.logger.error(`Error persisting connection for user: ${(error as Error).message}`, {
userId: dto.userUuid,
connectionId: dto.connectionId,
})
return Result.fail((error as Error).message)
}

View File

@@ -22,7 +22,9 @@ export class RemoveWebSocketsConnection implements UseCaseInterface<void> {
return Result.ok()
} catch (error) {
this.logger.error(`Error removing connection ${dto.connectionId}: ${(error as Error).message}`)
this.logger.error(`Error removing connection: ${(error as Error).message}`, {
connectionId: dto.connectionId,
})
return Result.fail((error as Error).message)
}

View File

@@ -1,4 +1,8 @@
import { ApiGatewayManagementApiClient } from '@aws-sdk/client-apigatewaymanagementapi'
import {
ApiGatewayManagementApiClient,
ApiGatewayManagementApiServiceException,
GoneException,
} from '@aws-sdk/client-apigatewaymanagementapi'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { SendMessageToClient } from './SendMessageToClient'
import { Logger } from 'winston'
@@ -23,6 +27,7 @@ describe('SendMessageToClient', () => {
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
webSocketsConnectionRepository.findAllByUserUuid = jest.fn().mockResolvedValue([connection])
webSocketsConnectionRepository.removeConnection = jest.fn()
apiGatewayManagementClient = {} as jest.Mocked<ApiGatewayManagementApiClient>
apiGatewayManagementClient.send = jest.fn().mockResolvedValue({ $metadata: { httpStatusCode: 200 } })
@@ -30,6 +35,7 @@ describe('SendMessageToClient', () => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.error = jest.fn()
logger.info = jest.fn()
})
it('sends message to all connections for a user', async () => {
@@ -96,4 +102,26 @@ describe('SendMessageToClient', () => {
expect(result.isFailed()).toBe(true)
})
it('removes connection if it is gone', async () => {
apiGatewayManagementClient.send = jest.fn().mockRejectedValue(
new GoneException(
new ApiGatewayManagementApiServiceException({
name: 'test',
$fault: 'server',
$metadata: {},
}),
),
)
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
message: 'message',
})
expect(result.isFailed()).toBe(false)
expect(webSocketsConnectionRepository.removeConnection).toHaveBeenCalledTimes(1)
})
})

View File

@@ -1,5 +1,9 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { ApiGatewayManagementApiClient, PostToConnectionCommand } from '@aws-sdk/client-apigatewaymanagementapi'
import {
ApiGatewayManagementApiClient,
GoneException,
PostToConnectionCommand,
} from '@aws-sdk/client-apigatewaymanagementapi'
import { Logger } from 'winston'
import { SendMessageToClientDTO } from './SendMessageToClientDTO'
@@ -44,6 +48,14 @@ export class SendMessageToClient implements UseCaseInterface<void> {
)
}
} catch (error) {
if (error instanceof GoneException) {
this.logger.debug(`Connection ${connection.props.connectionId} for user ${userUuid.value} is gone. Removing.`)
await this.removeGoneConnection(connection.props.connectionId)
return Result.ok()
}
return Result.fail(
`Could not send message to connection ${connection.props.connectionId} for user ${
userUuid.value
@@ -54,4 +66,8 @@ export class SendMessageToClient implements UseCaseInterface<void> {
return Result.ok()
}
private async removeGoneConnection(connectionId: string): Promise<void> {
await this.webSocketsConnectionRepository.removeConnection(connectionId)
}
}