mirror of
https://github.com/standardnotes/server
synced 2026-01-17 05:04:27 -05:00
Compare commits
35 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f275b48770 | ||
|
|
de4fcf9a4c | ||
|
|
a1455d281f | ||
|
|
cfbe2bbac6 | ||
|
|
398c10ce4b | ||
|
|
c7d21b092d | ||
|
|
031fa71e7d | ||
|
|
948e843ad6 | ||
|
|
7b0ea0a069 | ||
|
|
8887b6e642 | ||
|
|
597ff13393 | ||
|
|
4ab61b94a4 | ||
|
|
e19652d62a | ||
|
|
a341e78909 | ||
|
|
48e52ac48c | ||
|
|
6dbb87708f | ||
|
|
d15d51eae6 | ||
|
|
0058368681 | ||
|
|
746c821698 | ||
|
|
3f2d8c902c | ||
|
|
3637db2563 | ||
|
|
8ac84c59af | ||
|
|
a2b1323568 | ||
|
|
d35391288d | ||
|
|
d5c1b76de0 | ||
|
|
4600a49e88 | ||
|
|
96a2a67aa6 | ||
|
|
bb380b5794 | ||
|
|
47004fd20a | ||
|
|
c9bf024109 | ||
|
|
529795d393 | ||
|
|
79ae07623f | ||
|
|
6bdb524489 | ||
|
|
480693fb9f | ||
|
|
e150930072 |
14
.pnp.cjs
generated
14
.pnp.cjs
generated
@@ -2190,10 +2190,10 @@ const RAW_RUNTIME_STATE =
|
||||
}]\
|
||||
]],\
|
||||
["@grpc/grpc-js", [\
|
||||
["npm:1.9.11", {\
|
||||
"packageLocation": "./.yarn/cache/@grpc-grpc-js-npm-1.9.11-5bb7febd65-71b8517b4f.zip/node_modules/@grpc/grpc-js/",\
|
||||
["npm:1.9.12", {\
|
||||
"packageLocation": "./.yarn/cache/@grpc-grpc-js-npm-1.9.12-cb97be6754-fe13b04844.zip/node_modules/@grpc/grpc-js/",\
|
||||
"packageDependencies": [\
|
||||
["@grpc/grpc-js", "npm:1.9.11"],\
|
||||
["@grpc/grpc-js", "npm:1.9.12"],\
|
||||
["@grpc/proto-loader", "npm:0.7.10"],\
|
||||
["@types/node", "npm:20.2.5"]\
|
||||
],\
|
||||
@@ -5532,7 +5532,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./packages/api-gateway/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
|
||||
["@grpc/grpc-js", "npm:1.9.11"],\
|
||||
["@grpc/grpc-js", "npm:1.9.12"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
@@ -5582,7 +5582,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@aws-sdk/client-sqs", "npm:3.462.0"],\
|
||||
["@cbor-extract/cbor-extract-linux-arm64", "npm:2.1.1"],\
|
||||
["@cbor-extract/cbor-extract-linux-x64", "npm:2.1.1"],\
|
||||
["@grpc/grpc-js", "npm:1.9.11"],\
|
||||
["@grpc/grpc-js", "npm:1.9.12"],\
|
||||
["@simplewebauthn/server", "npm:8.1.1"],\
|
||||
["@simplewebauthn/typescript-types", "npm:8.0.0"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
@@ -5809,7 +5809,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./packages/grpc/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/grpc", "workspace:packages/grpc"],\
|
||||
["@grpc/grpc-js", "npm:1.9.11"],\
|
||||
["@grpc/grpc-js", "npm:1.9.12"],\
|
||||
["@types/google-protobuf", "npm:3.15.10"],\
|
||||
["google-protobuf", "npm:3.21.2"],\
|
||||
["grpc-tools", "npm:1.12.4"],\
|
||||
@@ -6082,7 +6082,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@aws-sdk/client-s3", "npm:3.462.0"],\
|
||||
["@aws-sdk/client-sns", "npm:3.462.0"],\
|
||||
["@aws-sdk/client-sqs", "npm:3.462.0"],\
|
||||
["@grpc/grpc-js", "npm:1.9.11"],\
|
||||
["@grpc/grpc-js", "npm:1.9.12"],\
|
||||
["@standardnotes/api", "npm:1.26.26"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
|
||||
Binary file not shown.
@@ -1,5 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# usage: file_env VAR [DEFAULT]
|
||||
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
|
||||
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
|
||||
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
|
||||
file_env() {
|
||||
local var="$1"
|
||||
local fileVar="${var}_FILE"
|
||||
local def="${2:-}"
|
||||
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
|
||||
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
|
||||
exit 1
|
||||
fi
|
||||
local val="$def"
|
||||
if [ "${!var:-}" ]; then
|
||||
val="${!var}"
|
||||
elif [ "${!fileVar:-}" ]; then
|
||||
val="$(< "${!fileVar}")"
|
||||
fi
|
||||
export "$var"="$val"
|
||||
unset "$fileVar"
|
||||
}
|
||||
|
||||
# Setup environment variables
|
||||
|
||||
export MODE="self-hosted"
|
||||
@@ -44,10 +66,12 @@ if [ -z "$DB_PORT" ]; then
|
||||
echo "DB_PORT is not set. Please set it in your .env file."
|
||||
exit 1
|
||||
fi
|
||||
file_env 'DB_USERNAME'
|
||||
if [ -z "$DB_USERNAME" ]; then
|
||||
echo "DB_USERNAME is not set. Please set it in your .env file."
|
||||
exit 1
|
||||
fi
|
||||
file_env 'DB_PASSWORD'
|
||||
if [ -z "$DB_PASSWORD" ]; then
|
||||
echo "DB_PASSWORD is not set. Please set it in your .env file."
|
||||
exit 1
|
||||
@@ -89,11 +113,13 @@ fi
|
||||
# SHARED #
|
||||
##########
|
||||
|
||||
file_env 'AUTH_JWT_SECRET'
|
||||
if [ -z "$AUTH_JWT_SECRET" ]; then
|
||||
echo "AUTH_JWT_SECRET is not set. Please set it in your .env file. You can run 'openssl rand -hex 32' to generate a random string."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file_env 'VALET_TOKEN_SECRET'
|
||||
if [ -z "$VALET_TOKEN_SECRET" ]; then
|
||||
echo "VALET_TOKEN_SECRET is not set. Please set it in your .env file. You can run 'openssl rand -hex 32' to generate a random string."
|
||||
exit 1
|
||||
@@ -120,6 +146,7 @@ if [ -z "$AUTH_SERVER_DISABLE_USER_REGISTRATION" ]; then
|
||||
export AUTH_SERVER_DISABLE_USER_REGISTRATION=false
|
||||
fi
|
||||
|
||||
file_env 'AUTH_SERVER_PSEUDO_KEY_PARAMS_KEY'
|
||||
if [ -z "$AUTH_SERVER_PSEUDO_KEY_PARAMS_KEY" ]; then
|
||||
export AUTH_SERVER_PSEUDO_KEY_PARAMS_KEY=$(openssl rand -hex 32)
|
||||
fi
|
||||
@@ -142,6 +169,7 @@ if [ -z "$AUTH_SERVER_EPHEMERAL_SESSION_AGE" ]; then
|
||||
export AUTH_SERVER_EPHEMERAL_SESSION_AGE=259200
|
||||
fi
|
||||
|
||||
file_env 'AUTH_SERVER_ENCRYPTION_SERVER_KEY'
|
||||
if [ -z "$AUTH_SERVER_ENCRYPTION_SERVER_KEY" ]; then
|
||||
echo "AUTH_SERVER_ENCRYPTION_SERVER_KEY is not set. Please set it in your .env file. You can run 'openssl rand -hex 32' to generate a random string."
|
||||
exit 1
|
||||
@@ -161,9 +189,11 @@ fi
|
||||
if [ -z "$AUTH_SERVER_SNS_ENDPOINT" ]; then
|
||||
export AUTH_SERVER_SNS_ENDPOINT="http://localstack:4566"
|
||||
fi
|
||||
file_env 'AUTH_SERVER_SNS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$AUTH_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
|
||||
export AUTH_SERVER_SNS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
file_env 'AUTH_SERVER_SNS_ACCESS_KEY_ID'
|
||||
if [ -z "$AUTH_SERVER_SNS_ACCESS_KEY_ID" ]; then
|
||||
export AUTH_SERVER_SNS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
@@ -176,9 +206,11 @@ fi
|
||||
if [ -z "$AUTH_SERVER_SQS_AWS_REGION" ]; then
|
||||
export AUTH_SERVER_SQS_AWS_REGION="us-east-1"
|
||||
fi
|
||||
file_env 'AUTH_SERVER_SQS_ACCESS_KEY_ID'
|
||||
if [ -z "$AUTH_SERVER_SQS_ACCESS_KEY_ID" ]; then
|
||||
export AUTH_SERVER_SQS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
file_env 'AUTH_SERVER_SQS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$AUTH_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
|
||||
export AUTH_SERVER_SQS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
@@ -218,9 +250,11 @@ fi
|
||||
if [ -z "$SYNCING_SERVER_SNS_ENDPOINT" ]; then
|
||||
export SYNCING_SERVER_SNS_ENDPOINT="http://localstack:4566"
|
||||
fi
|
||||
file_env 'SYNCING_SERVER_SNS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$SYNCING_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
|
||||
export SYNCING_SERVER_SNS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
file_env 'SYNCING_SERVER_SNS_ACCESS_KEY_ID'
|
||||
if [ -z "$SYNCING_SERVER_SNS_ACCESS_KEY_ID" ]; then
|
||||
export SYNCING_SERVER_SNS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
@@ -233,9 +267,11 @@ fi
|
||||
if [ -z "$SYNCING_SERVER_SQS_AWS_REGION" ]; then
|
||||
export SYNCING_SERVER_SQS_AWS_REGION="us-east-1"
|
||||
fi
|
||||
file_env 'SYNCING_SERVER_SQS_ACCESS_KEY_ID'
|
||||
if [ -z "$SYNCING_SERVER_SQS_ACCESS_KEY_ID" ]; then
|
||||
export SYNCING_SERVER_SQS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
file_env 'SYNCING_SERVER_SQS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$SYNCING_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
|
||||
export SYNCING_SERVER_SQS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
@@ -278,9 +314,11 @@ fi
|
||||
if [ -z "$FILES_SERVER_SNS_ENDPOINT" ]; then
|
||||
export FILES_SERVER_SNS_ENDPOINT="http://localstack:4566"
|
||||
fi
|
||||
file_env 'FILES_SERVER_SNS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$FILES_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
|
||||
export FILES_SERVER_SNS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
file_env 'FILES_SERVER_SNS_ACCESS_KEY_ID'
|
||||
if [ -z "$FILES_SERVER_SNS_ACCESS_KEY_ID" ]; then
|
||||
export FILES_SERVER_SNS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
@@ -293,9 +331,11 @@ fi
|
||||
if [ -z "$FILES_SERVER_SQS_AWS_REGION" ]; then
|
||||
export FILES_SERVER_SQS_AWS_REGION="us-east-1"
|
||||
fi
|
||||
file_env 'FILES_SERVER_SQS_ACCESS_KEY_ID'
|
||||
if [ -z "$FILES_SERVER_SQS_ACCESS_KEY_ID" ]; then
|
||||
export FILES_SERVER_SQS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
file_env 'FILES_SERVER_SQS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$FILES_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
|
||||
export FILES_SERVER_SQS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
@@ -322,9 +362,11 @@ fi
|
||||
if [ -z "$REVISIONS_SERVER_SNS_ENDPOINT" ]; then
|
||||
export REVISIONS_SERVER_SNS_ENDPOINT="http://localstack:4566"
|
||||
fi
|
||||
file_env 'REVISIONS_SERVER_SNS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$REVISIONS_SERVER_SNS_SECRET_ACCESS_KEY" ]; then
|
||||
export REVISIONS_SERVER_SNS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
file_env 'REVISIONS_SERVER_SNS_ACCESS_KEY_ID'
|
||||
if [ -z "$REVISIONS_SERVER_SNS_ACCESS_KEY_ID" ]; then
|
||||
export REVISIONS_SERVER_SNS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
@@ -337,9 +379,11 @@ fi
|
||||
if [ -z "$REVISIONS_SERVER_SQS_AWS_REGION" ]; then
|
||||
export REVISIONS_SERVER_SQS_AWS_REGION="us-east-1"
|
||||
fi
|
||||
file_env 'REVISIONS_SERVER_SQS_ACCESS_KEY_ID'
|
||||
if [ -z "$REVISIONS_SERVER_SQS_ACCESS_KEY_ID" ]; then
|
||||
export REVISIONS_SERVER_SQS_ACCESS_KEY_ID="x"
|
||||
fi
|
||||
file_env 'REVISIONS_SERVER_SQS_SECRET_ACCESS_KEY'
|
||||
if [ -z "$REVISIONS_SERVER_SQS_SECRET_ACCESS_KEY" ]; then
|
||||
export REVISIONS_SERVER_SQS_SECRET_ACCESS_KEY="x"
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.34.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.34.6...@standardnotes/analytics@2.34.7) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.34.4",
|
||||
"version": "2.34.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -3,6 +3,55 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.89.1](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.0...@standardnotes/api-gateway@1.89.1) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
# [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
|
||||
|
||||
## [1.87.6](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.87.5...@standardnotes/api-gateway@1.87.6) (2023-12-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** add grpc logs for internal errors ([529795d](https://github.com/standardnotes/server/commit/529795d393442727833f318234d308543c1ea731))
|
||||
|
||||
## [1.87.5](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.87.4...@standardnotes/api-gateway@1.87.5) (2023-12-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.87.5",
|
||||
"version": "1.89.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -31,7 +31,7 @@
|
||||
"start": "yarn node dist/bin/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.9.11",
|
||||
"@grpc/grpc-js": "^1.9.12",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
|
||||
@@ -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(),
|
||||
{
|
||||
@@ -210,6 +203,7 @@ export class ContainerConfigLoader {
|
||||
container.get<MapperInterface<SyncResponse, SyncResponseHttpRepresentation>>(
|
||||
TYPES.Mapper_SyncResponseGRPCMapper,
|
||||
),
|
||||
container.get<winston.Logger>(TYPES.ApiGateway_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
@@ -228,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),
|
||||
),
|
||||
)
|
||||
@@ -237,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
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)}`)
|
||||
|
||||
@@ -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)}`)
|
||||
|
||||
@@ -4,12 +4,15 @@ import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { Metadata } from '@grpc/grpc-js'
|
||||
|
||||
import { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation'
|
||||
import { Status } from '@grpc/grpc-js/build/src/constants'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class GRPCSyncingServerServiceProxy {
|
||||
constructor(
|
||||
private syncingClient: ISyncingClient,
|
||||
private syncRequestGRPCMapper: MapperInterface<Record<string, unknown>, SyncRequest>,
|
||||
private syncResponseGRPCMapper: MapperInterface<SyncResponse, SyncResponseHttpRepresentation>,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async sync(
|
||||
@@ -28,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) {
|
||||
@@ -39,12 +43,29 @@ export class GRPCSyncingServerServiceProxy {
|
||||
})
|
||||
}
|
||||
|
||||
if (error.code === Status.INTERNAL) {
|
||||
this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, {
|
||||
codeTag: 'GRPCSyncingServerServiceProxy',
|
||||
userId: response.locals.user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
return resolve({ status: 200, data: this.syncResponseGRPCMapper.toProjection(syncResponse) })
|
||||
})
|
||||
} catch (error) {
|
||||
if (
|
||||
'code' in (error as Record<string, unknown>) &&
|
||||
(error as Record<string, unknown>).code === Status.INTERNAL
|
||||
) {
|
||||
this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, {
|
||||
codeTag: 'GRPCSyncingServerServiceProxy.catch',
|
||||
userId: response.locals.user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.177.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.176.5...@standardnotes/auth-server@1.177.0) (2023-12-14)
|
||||
|
||||
### Features
|
||||
|
||||
* add procedure for recalculating file quota for user ([#980](https://github.com/standardnotes/server/issues/980)) ([de4fcf9](https://github.com/standardnotes/server/commit/de4fcf9a4c308ad7d71c42fe5c27af18b8614e1a))
|
||||
|
||||
## [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
|
||||
|
||||
# [1.175.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.174.4...@standardnotes/auth-server@1.175.0) (2023-12-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add renewal of shared subscriptions ([#952](https://github.com/standardnotes/server/issues/952)) ([e150930](https://github.com/standardnotes/server/commit/e15093007264489cf975a4d454e4a19e84bb13b7))
|
||||
|
||||
## [1.174.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.174.3...@standardnotes/auth-server@1.174.4) (2023-12-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
45
packages/auth/bin/fix_quota.ts
Normal file
45
packages/auth/bin/fix_quota.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import { FixStorageQuotaForUser } from '../src/Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
|
||||
|
||||
const inputArgs = process.argv.slice(2)
|
||||
const userEmail = inputArgs[0]
|
||||
|
||||
const container = new ContainerConfigLoader('worker')
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Auth_Logger)
|
||||
|
||||
logger.info('Starting storage quota fix...', {
|
||||
userId: userEmail,
|
||||
})
|
||||
|
||||
const fixStorageQuota = container.get<FixStorageQuotaForUser>(TYPES.Auth_FixStorageQuotaForUser)
|
||||
|
||||
Promise.resolve(
|
||||
fixStorageQuota.execute({
|
||||
userEmail,
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info('Storage quota fixed', {
|
||||
userId: userEmail,
|
||||
})
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not fix storage quota: ${error.message}`, {
|
||||
userId: userEmail,
|
||||
})
|
||||
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
@@ -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) {
|
||||
|
||||
11
packages/auth/docker/entrypoint-fix-quota.js
Normal file
11
packages/auth/docker/entrypoint-fix-quota.js
Normal file
@@ -0,0 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
|
||||
|
||||
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/fix_quota.js')))
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true })
|
||||
|
||||
exports.default = index
|
||||
@@ -5,43 +5,40 @@ COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-web' )
|
||||
echo "[Docker] Starting Web..."
|
||||
exec node docker/entrypoint-server.js
|
||||
;;
|
||||
|
||||
'start-worker' )
|
||||
echo "[Docker] Starting Worker..."
|
||||
exec node docker/entrypoint-worker.js
|
||||
;;
|
||||
|
||||
'cleanup' )
|
||||
echo "[Docker] Starting Cleanup..."
|
||||
exec node docker/entrypoint-cleanup.js
|
||||
;;
|
||||
|
||||
'stats' )
|
||||
echo "[Docker] Starting Persisting Stats..."
|
||||
exec node docker/entrypoint-stats.js
|
||||
;;
|
||||
|
||||
'email-daily-backup' )
|
||||
echo "[Docker] Starting Email Daily Backup..."
|
||||
exec node docker/entrypoint-backup.js daily
|
||||
;;
|
||||
|
||||
'email-weekly-backup' )
|
||||
echo "[Docker] Starting Email Weekly Backup..."
|
||||
exec node docker/entrypoint-backup.js weekly
|
||||
;;
|
||||
|
||||
'email-backup' )
|
||||
echo "[Docker] Starting Email Backup For Single User..."
|
||||
EMAIL=$1 && shift 1
|
||||
exec node docker/entrypoint-user-email-backup.js $EMAIL
|
||||
;;
|
||||
|
||||
'fix-quota' )
|
||||
EMAIL=$1 && shift 1
|
||||
exec node docker/entrypoint-fix-quota.js $EMAIL
|
||||
;;
|
||||
|
||||
'delete-accounts' )
|
||||
echo "[Docker] Starting Accounts Deleting from CSV..."
|
||||
FILE_NAME=$1 && shift 1
|
||||
MODE=$1 && shift 1
|
||||
exec node docker/entrypoint-delete-accounts.js $FILE_NAME $MODE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.174.4",
|
||||
"version": "1.177.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -43,7 +43,7 @@
|
||||
"@aws-sdk/client-sqs": "^3.462.0",
|
||||
"@cbor-extract/cbor-extract-linux-arm64": "^2.1.1",
|
||||
"@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
|
||||
"@grpc/grpc-js": "^1.9.11",
|
||||
"@grpc/grpc-js": "^1.9.12",
|
||||
"@simplewebauthn/server": "^8.1.1",
|
||||
"@simplewebauthn/typescript-types": "^8.0.0",
|
||||
"@standardnotes/api": "^1.26.26",
|
||||
|
||||
@@ -281,6 +281,9 @@ import { CSVFileReaderInterface } from '../Domain/CSV/CSVFileReaderInterface'
|
||||
import { S3CsvFileReader } from '../Infra/S3/S3CsvFileReader'
|
||||
import { DeleteAccountsFromCSVFile } from '../Domain/UseCase/DeleteAccountsFromCSVFile/DeleteAccountsFromCSVFile'
|
||||
import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler/AccountDeletionVerificationPassedEventHandler'
|
||||
import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
|
||||
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
|
||||
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -309,7 +312,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)
|
||||
@@ -1268,6 +1271,34 @@ 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
|
||||
.bind<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions)
|
||||
.toConstantValue(
|
||||
new RenewSharedSubscriptions(
|
||||
container.get<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations),
|
||||
container.get<SharedSubscriptionInvitationRepositoryInterface>(
|
||||
TYPES.Auth_SharedSubscriptionInvitationRepository,
|
||||
),
|
||||
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FixStorageQuotaForUser>(TYPES.Auth_FixStorageQuotaForUser)
|
||||
.toConstantValue(
|
||||
new FixStorageQuotaForUser(
|
||||
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
|
||||
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
|
||||
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
|
||||
container.get<SetSubscriptionSettingValue>(TYPES.Auth_SetSubscriptionSettingValue),
|
||||
container.get<ListSharedSubscriptionInvitations>(TYPES.Auth_ListSharedSubscriptionInvitations),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
if (!isConfiguredForHomeServer) {
|
||||
@@ -1349,6 +1380,7 @@ export class ContainerConfigLoader {
|
||||
container.get<ApplyDefaultSubscriptionSettings>(TYPES.Auth_ApplyDefaultSubscriptionSettings),
|
||||
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
|
||||
container.get<RoleServiceInterface>(TYPES.Auth_RoleService),
|
||||
container.get<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
@@ -1525,6 +1557,14 @@ export class ContainerConfigLoader {
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler)
|
||||
.toConstantValue(
|
||||
new FileQuotaRecalculatedEventHandler(
|
||||
container.get<UpdateStorageQuotaUsedForUser>(TYPES.Auth_UpdateStorageQuotaUsedForUser),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Auth_AccountDeletionRequestedEventHandler)],
|
||||
@@ -1562,6 +1602,10 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler),
|
||||
],
|
||||
['USER_INVITED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserInvitedToSharedVaultEventHandler)],
|
||||
[
|
||||
'FILE_QUOTA_RECALCULATED',
|
||||
container.get<FileQuotaRecalculatedEventHandler>(TYPES.Auth_FileQuotaRecalculatedEventHandler),
|
||||
],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
|
||||
@@ -169,6 +169,8 @@ const TYPES = {
|
||||
Auth_TriggerEmailBackupForUser: Symbol.for('Auth_TriggerEmailBackupForUser'),
|
||||
Auth_TriggerEmailBackupForAllUsers: Symbol.for('Auth_TriggerEmailBackupForAllUsers'),
|
||||
Auth_DeleteAccountsFromCSVFile: Symbol.for('Auth_DeleteAccountsFromCSVFile'),
|
||||
Auth_RenewSharedSubscriptions: Symbol.for('Auth_RenewSharedSubscriptions'),
|
||||
Auth_FixStorageQuotaForUser: Symbol.for('Auth_FixStorageQuotaForUser'),
|
||||
// Handlers
|
||||
Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
|
||||
Auth_AccountDeletionVerificationPassedEventHandler: Symbol.for('Auth_AccountDeletionVerificationPassedEventHandler'),
|
||||
@@ -202,6 +204,7 @@ const TYPES = {
|
||||
'Auth_UserDesignatedAsSurvivorInSharedVaultEventHandler',
|
||||
),
|
||||
Auth_UserInvitedToSharedVaultEventHandler: Symbol.for('Auth_UserInvitedToSharedVaultEventHandler'),
|
||||
Auth_FileQuotaRecalculatedEventHandler: Symbol.for('Auth_FileQuotaRecalculatedEventHandler'),
|
||||
// Services
|
||||
Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
|
||||
Auth_SessionService: Symbol.for('Auth_SessionService'),
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
SessionCreatedEvent,
|
||||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
@@ -34,6 +35,21 @@ import { KeyParamsData } from '@standardnotes/responses'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Auth_Timer) private timer: TimerInterface) {}
|
||||
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent {
|
||||
return {
|
||||
type: 'FILE_QUOTA_RECALCULATION_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Auth,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
|
||||
createAccountDeletionVerificationRequestedEvent(dto: {
|
||||
userUuid: string
|
||||
email: string
|
||||
@@ -159,6 +175,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_REQUESTED',
|
||||
|
||||
@@ -19,11 +19,13 @@ import {
|
||||
SessionCreatedEvent,
|
||||
SessionRefreshedEvent,
|
||||
AccountDeletionVerificationRequestedEvent,
|
||||
FileQuotaRecalculationRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createFileQuotaRecalculationRequestedEvent(dto: { userUuid: string }): FileQuotaRecalculationRequestedEvent
|
||||
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
@@ -31,6 +33,7 @@ export interface DomainEventFactoryInterface {
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent
|
||||
createListedAccountRequestedEvent(userUuid: string, userEmail: string): ListedAccountRequestedEvent
|
||||
createUserRegisteredEvent(dto: {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { DomainEventHandlerInterface, FileQuotaRecalculatedEvent } from '@standardnotes/domain-events'
|
||||
import { UpdateStorageQuotaUsedForUser } from '../UseCase/UpdateStorageQuotaUsedForUser/UpdateStorageQuotaUsedForUser'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class FileQuotaRecalculatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private updateStorageQuota: UpdateStorageQuotaUsedForUser,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: FileQuotaRecalculatedEvent): Promise<void> {
|
||||
this.logger.info('Updating storage quota for user...', {
|
||||
userId: event.payload.userUuid,
|
||||
totalFileByteSize: event.payload.totalFileByteSize,
|
||||
codeTag: 'FileQuotaRecalculatedEventHandler',
|
||||
})
|
||||
|
||||
const result = await this.updateStorageQuota.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
bytesUsed: event.payload.totalFileByteSize,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error('Could not update storage quota', {
|
||||
userId: event.payload.userUuid,
|
||||
codeTag: 'FileQuotaRecalculatedEventHandler',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info('Storage quota updated', {
|
||||
userId: event.payload.userUuid,
|
||||
totalFileByteSize: event.payload.totalFileByteSize,
|
||||
codeTag: 'FileQuotaRecalculatedEventHandler',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { RenewSharedSubscriptions } from '../UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
|
||||
|
||||
export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@@ -19,6 +20,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
private applyDefaultSubscriptionSettings: ApplyDefaultSubscriptionSettings,
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
private roleService: RoleServiceInterface,
|
||||
private renewSharedSubscriptions: RenewSharedSubscriptions,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -58,6 +60,17 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
event.payload.timestamp,
|
||||
)
|
||||
|
||||
const renewalResult = await this.renewSharedSubscriptions.execute({
|
||||
inviterEmail: user.email,
|
||||
newSubscriptionId: event.payload.subscriptionId,
|
||||
newSubscriptionName: event.payload.subscriptionName,
|
||||
newSubscriptionExpiresAt: event.payload.subscriptionExpiresAt,
|
||||
timestamp: event.payload.timestamp,
|
||||
})
|
||||
if (renewalResult.isFailed()) {
|
||||
this.logger.error(`Could not renew shared subscriptions for user ${user.uuid}: ${renewalResult.getError()}`)
|
||||
}
|
||||
|
||||
await this.addUserRole(user, event.payload.subscriptionName)
|
||||
|
||||
const result = await this.applyDefaultSubscriptionSettings.execute({
|
||||
|
||||
@@ -35,6 +35,7 @@ export class UserInvitedToSharedVaultEventHandler implements DomainEventHandlerI
|
||||
subject: getSubject(),
|
||||
messageIdentifier: 'USER_INVITED_TO_SHARED_VAULT',
|
||||
userEmail: user.email,
|
||||
userUuid: user.uuid,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { DomainEventPublisherInterface, FileQuotaRecalculationRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
|
||||
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue'
|
||||
import { FixStorageQuotaForUser } from './FixStorageQuotaForUser'
|
||||
import { User } from '../../User/User'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
|
||||
|
||||
describe('FixStorageQuotaForUser', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let getRegularSubscription: GetRegularSubscriptionForUser
|
||||
let getSharedSubscriptionForUser: GetSharedSubscriptionForUser
|
||||
let setSubscriptonSettingValue: SetSubscriptionSettingValue
|
||||
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new FixStorageQuotaForUser(
|
||||
userRepository,
|
||||
getRegularSubscription,
|
||||
getSharedSubscriptionForUser,
|
||||
setSubscriptonSettingValue,
|
||||
listSharedSubscriptionInvitations,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue({
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
} as jest.Mocked<User>)
|
||||
|
||||
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
} as jest.Mocked<UserSubscription>),
|
||||
)
|
||||
|
||||
getSharedSubscriptionForUser = {} as jest.Mocked<GetSharedSubscriptionForUser>
|
||||
getSharedSubscriptionForUser.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
} as jest.Mocked<UserSubscription>),
|
||||
)
|
||||
|
||||
setSubscriptonSettingValue = {} as jest.Mocked<SetSubscriptionSettingValue>
|
||||
setSubscriptonSettingValue.execute = jest.fn().mockReturnValue(Result.ok(Result.ok()))
|
||||
|
||||
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
|
||||
listSharedSubscriptionInvitations.execute = jest.fn().mockReturnValue({
|
||||
invitations: [
|
||||
{
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
status: InvitationStatus.Accepted,
|
||||
inviteeIdentifier: 'test2@test.te',
|
||||
} as jest.Mocked<SharedSubscriptionInvitation>,
|
||||
],
|
||||
})
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createFileQuotaRecalculationRequestedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<FileQuotaRecalculationRequestedEvent>)
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error result if user cannot be found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error result if regular subscription cannot be found', async () => {
|
||||
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('test'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error result if shared subscription cannot be found', async () => {
|
||||
getSharedSubscriptionForUser.execute = jest.fn().mockReturnValue(Result.fail('test'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error result if setting value cannot be set', async () => {
|
||||
setSubscriptonSettingValue.execute = jest.fn().mockReturnValue(Result.fail('test'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should reset storage quota and ask for recalculation for user and all its shared subscriptions', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should return error if the username is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: '',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if the invitee username is invalid', async () => {
|
||||
listSharedSubscriptionInvitations.execute = jest.fn().mockReturnValue({
|
||||
invitations: [
|
||||
{
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
status: InvitationStatus.Accepted,
|
||||
inviteeIdentifier: '',
|
||||
} as jest.Mocked<SharedSubscriptionInvitation>,
|
||||
],
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if the invitee cannot be found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
} as jest.Mocked<User>)
|
||||
.mockReturnValueOnce(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should return error if fails to reset storage quota for the invitee', async () => {
|
||||
setSubscriptonSettingValue.execute = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(Result.ok())
|
||||
.mockReturnValueOnce(Result.fail('test'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userEmail: 'test@test.te',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Result, SettingName, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
|
||||
import { FixStorageQuotaForUserDTO } from './FixStorageQuotaForUserDTO'
|
||||
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
|
||||
import { SetSubscriptionSettingValue } from '../SetSubscriptionSettingValue/SetSubscriptionSettingValue'
|
||||
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class FixStorageQuotaForUser implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private getRegularSubscription: GetRegularSubscriptionForUser,
|
||||
private getSharedSubscriptionForUser: GetSharedSubscriptionForUser,
|
||||
private setSubscriptonSettingValue: SetSubscriptionSettingValue,
|
||||
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: FixStorageQuotaForUserDTO): Promise<Result<void>> {
|
||||
const usernameOrError = Username.create(dto.userEmail)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return Result.fail(usernameOrError.getError())
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
if (user === null) {
|
||||
return Result.fail(`Could not find user with email: ${username.value}`)
|
||||
}
|
||||
|
||||
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
if (regularSubscriptionOrError.isFailed()) {
|
||||
return Result.fail(`Could not find regular user subscription for user with uuid: ${user.uuid}`)
|
||||
}
|
||||
const regularSubscription = regularSubscriptionOrError.getValue()
|
||||
|
||||
const result = await this.setSubscriptonSettingValue.execute({
|
||||
userSubscriptionUuid: regularSubscription.uuid,
|
||||
settingName: SettingName.NAMES.FileUploadBytesUsed,
|
||||
value: '0',
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
this.logger.info('Resetted storage quota for user', {
|
||||
userId: user.uuid,
|
||||
})
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileQuotaRecalculationRequestedEvent({
|
||||
userUuid: user.uuid,
|
||||
}),
|
||||
)
|
||||
|
||||
this.logger.info('Requested storage quota recalculation for user', {
|
||||
userId: user.uuid,
|
||||
})
|
||||
|
||||
const invitationsResult = await this.listSharedSubscriptionInvitations.execute({
|
||||
inviterEmail: user.email,
|
||||
})
|
||||
const acceptedInvitations = invitationsResult.invitations.filter(
|
||||
(invitation) => invitation.status === InvitationStatus.Accepted,
|
||||
)
|
||||
for (const invitation of acceptedInvitations) {
|
||||
const inviteeUsernameOrError = Username.create(invitation.inviteeIdentifier)
|
||||
if (inviteeUsernameOrError.isFailed()) {
|
||||
return Result.fail(inviteeUsernameOrError.getError())
|
||||
}
|
||||
const inviteeUsername = inviteeUsernameOrError.getValue()
|
||||
|
||||
const invitee = await this.userRepository.findOneByUsernameOrEmail(inviteeUsername)
|
||||
if (invitee === null) {
|
||||
return Result.fail(`Could not find user with email: ${inviteeUsername.value}`)
|
||||
}
|
||||
|
||||
const invitationSubscriptionOrError = await this.getSharedSubscriptionForUser.execute({
|
||||
userUuid: invitee.uuid,
|
||||
})
|
||||
if (invitationSubscriptionOrError.isFailed()) {
|
||||
return Result.fail(`Could not find shared subscription for user with email: ${invitation.inviteeIdentifier}`)
|
||||
}
|
||||
const invitationSubscription = invitationSubscriptionOrError.getValue()
|
||||
|
||||
const result = await this.setSubscriptonSettingValue.execute({
|
||||
userSubscriptionUuid: invitationSubscription.uuid,
|
||||
settingName: SettingName.NAMES.FileUploadBytesUsed,
|
||||
value: '0',
|
||||
})
|
||||
if (result.isFailed()) {
|
||||
return Result.fail(result.getError())
|
||||
}
|
||||
|
||||
this.logger.info('Resetted storage quota for user', {
|
||||
userId: invitee.uuid,
|
||||
})
|
||||
|
||||
await this.domainEventPublisher.publish(
|
||||
this.domainEventFactory.createFileQuotaRecalculationRequestedEvent({
|
||||
userUuid: invitee.uuid,
|
||||
}),
|
||||
)
|
||||
|
||||
this.logger.info('Requested storage quota recalculation for user', {
|
||||
userId: invitee.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface FixStorageQuotaForUserDTO {
|
||||
userEmail: string
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Logger } from 'winston'
|
||||
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
import { RenewSharedSubscriptions } from './RenewSharedSubscriptions'
|
||||
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
|
||||
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
|
||||
import { User } from '../../User/User'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
|
||||
describe('RenewSharedSubscriptions', () => {
|
||||
let listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations
|
||||
let sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let userRepository: UserRepositoryInterface
|
||||
let logger: Logger
|
||||
let sharedSubscriptionInvitation: SharedSubscriptionInvitation
|
||||
let user: User
|
||||
|
||||
const createUseCase = () =>
|
||||
new RenewSharedSubscriptions(
|
||||
listSharedSubscriptionInvitations,
|
||||
sharedSubscriptionInvitationRepository,
|
||||
userSubscriptionRepository,
|
||||
userRepository,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
user.uuid = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
sharedSubscriptionInvitation = {} as jest.Mocked<SharedSubscriptionInvitation>
|
||||
sharedSubscriptionInvitation.uuid = '00000000-0000-0000-0000-000000000000'
|
||||
sharedSubscriptionInvitation.inviteeIdentifier = 'test@test.te'
|
||||
sharedSubscriptionInvitation.inviteeIdentifierType = InviteeIdentifierType.Email
|
||||
sharedSubscriptionInvitation.status = InvitationStatus.Accepted
|
||||
|
||||
listSharedSubscriptionInvitations = {} as jest.Mocked<ListSharedSubscriptionInvitations>
|
||||
listSharedSubscriptionInvitations.execute = jest.fn().mockReturnValue({
|
||||
invitations: [sharedSubscriptionInvitation],
|
||||
})
|
||||
|
||||
sharedSubscriptionInvitationRepository = {} as jest.Mocked<SharedSubscriptionInvitationRepositoryInterface>
|
||||
sharedSubscriptionInvitationRepository.save = jest.fn()
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.save = jest.fn()
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should renew shared subscriptions', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviterEmail: 'inviter@test.te',
|
||||
newSubscriptionId: 123,
|
||||
newSubscriptionName: 'test',
|
||||
newSubscriptionExpiresAt: 123,
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(sharedSubscriptionInvitationRepository.save).toBeCalledTimes(1)
|
||||
expect(userSubscriptionRepository.save).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should log error if user not found', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviterEmail: 'inviter@test.te',
|
||||
newSubscriptionId: 123,
|
||||
newSubscriptionName: 'test',
|
||||
newSubscriptionExpiresAt: 123,
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should log error if error occurs', async () => {
|
||||
userRepository.findOneByUsernameOrEmail = jest.fn().mockImplementation(() => {
|
||||
throw new Error('test')
|
||||
})
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviterEmail: 'inviter@test.te',
|
||||
newSubscriptionId: 123,
|
||||
newSubscriptionName: 'test',
|
||||
newSubscriptionExpiresAt: 123,
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should log error if username is invalid', async () => {
|
||||
sharedSubscriptionInvitation.inviteeIdentifierType = InviteeIdentifierType.Email
|
||||
sharedSubscriptionInvitation.inviteeIdentifier = ''
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviterEmail: 'inviter@test.te',
|
||||
newSubscriptionId: 123,
|
||||
newSubscriptionName: 'test',
|
||||
newSubscriptionExpiresAt: 123,
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(logger.error).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should renew shared subscription for invitations by user uuid', async () => {
|
||||
sharedSubscriptionInvitation.inviteeIdentifierType = InviteeIdentifierType.Uuid
|
||||
sharedSubscriptionInvitation.inviteeIdentifier = '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
inviterEmail: 'inviter@test.te',
|
||||
newSubscriptionId: 123,
|
||||
newSubscriptionName: 'test',
|
||||
newSubscriptionExpiresAt: 123,
|
||||
timestamp: 123,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(sharedSubscriptionInvitationRepository.save).toBeCalledTimes(1)
|
||||
expect(userSubscriptionRepository.save).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Result, UseCaseInterface, Username } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { RenewSharedSubscriptionsDTO } from './RenewSharedSubscriptionsDTO'
|
||||
import { ListSharedSubscriptionInvitations } from '../ListSharedSubscriptionInvitations/ListSharedSubscriptionInvitations'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
import { SharedSubscriptionInvitationRepositoryInterface } from '../../SharedSubscription/SharedSubscriptionInvitationRepositoryInterface'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { UserSubscriptionRepositoryInterface } from '../../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { InviteeIdentifierType } from '../../SharedSubscription/InviteeIdentifierType'
|
||||
|
||||
export class RenewSharedSubscriptions implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private listSharedSubscriptionInvitations: ListSharedSubscriptionInvitations,
|
||||
private sharedSubscriptionInvitationRepository: SharedSubscriptionInvitationRepositoryInterface,
|
||||
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
private userRepository: UserRepositoryInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: RenewSharedSubscriptionsDTO): Promise<Result<void>> {
|
||||
const result = await this.listSharedSubscriptionInvitations.execute({
|
||||
inviterEmail: dto.inviterEmail,
|
||||
})
|
||||
|
||||
const acceptedInvitations = result.invitations.filter(
|
||||
(invitation) => invitation.status === InvitationStatus.Accepted,
|
||||
)
|
||||
|
||||
for (const invitation of acceptedInvitations) {
|
||||
try {
|
||||
const userUuid = await this.getInviteeUserUuid(invitation.inviteeIdentifier, invitation.inviteeIdentifierType)
|
||||
if (userUuid === null) {
|
||||
this.logger.error(
|
||||
`[SUBSCRIPTION: ${dto.newSubscriptionId}] Could not renew shared subscription for invitation: ${invitation.uuid}: Could not find user with identifier: ${invitation.inviteeIdentifier}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
await this.createSharedSubscription({
|
||||
subscriptionId: dto.newSubscriptionId,
|
||||
subscriptionName: dto.newSubscriptionName,
|
||||
userUuid,
|
||||
timestamp: dto.timestamp,
|
||||
subscriptionExpiresAt: dto.newSubscriptionExpiresAt,
|
||||
})
|
||||
|
||||
invitation.subscriptionId = dto.newSubscriptionId
|
||||
invitation.updatedAt = dto.timestamp
|
||||
|
||||
await this.sharedSubscriptionInvitationRepository.save(invitation)
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[SUBSCRIPTION: ${dto.newSubscriptionId}] Could not renew shared subscription for invitation: ${
|
||||
invitation.uuid
|
||||
}: ${(error as Error).message}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
|
||||
private async createSharedSubscription(dto: {
|
||||
subscriptionId: number
|
||||
subscriptionName: string
|
||||
userUuid: string
|
||||
subscriptionExpiresAt: number
|
||||
timestamp: number
|
||||
}): Promise<UserSubscription> {
|
||||
const subscription = new UserSubscription()
|
||||
subscription.planName = dto.subscriptionName
|
||||
subscription.userUuid = dto.userUuid
|
||||
subscription.createdAt = dto.timestamp
|
||||
subscription.updatedAt = dto.timestamp
|
||||
subscription.endsAt = dto.subscriptionExpiresAt
|
||||
subscription.cancelled = false
|
||||
subscription.subscriptionId = dto.subscriptionId
|
||||
subscription.subscriptionType = UserSubscriptionType.Shared
|
||||
|
||||
return this.userSubscriptionRepository.save(subscription)
|
||||
}
|
||||
|
||||
private async getInviteeUserUuid(inviteeIdentifier: string, inviteeIdentifierType: string): Promise<string | null> {
|
||||
if (inviteeIdentifierType === InviteeIdentifierType.Email) {
|
||||
const usernameOrError = Username.create(inviteeIdentifier)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return null
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
if (user === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return user.uuid
|
||||
}
|
||||
|
||||
return inviteeIdentifier
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface RenewSharedSubscriptionsDTO {
|
||||
inviterEmail: string
|
||||
newSubscriptionId: number
|
||||
newSubscriptionExpiresAt: number
|
||||
newSubscriptionName: string
|
||||
timestamp: number
|
||||
}
|
||||
@@ -131,6 +131,7 @@ export class SignIn implements UseCaseInterface {
|
||||
),
|
||||
messageIdentifier: 'SIGN_IN',
|
||||
subject: getSubject(user.email),
|
||||
userUuid: user.uuid,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -163,6 +163,20 @@ describe('UpdateStorageQuotaUsedForUser', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not subtract below 0', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
bytesUsed: -1234,
|
||||
})
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
|
||||
expect(setSubscriptonSettingValue.execute).toHaveBeenCalledWith({
|
||||
settingName: 'FILE_UPLOAD_BYTES_USED',
|
||||
value: '0',
|
||||
userSubscriptionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update a bytes used setting on both regular and shared subscription', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
|
||||
@@ -68,10 +68,13 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
|
||||
bytesAlreadyUsed = bytesUsedSetting.setting.props.value as string
|
||||
}
|
||||
|
||||
const bytesUsedNewTotal = +bytesAlreadyUsed + bytesUsed
|
||||
const bytesUsedValue = bytesUsedNewTotal < 0 ? 0 : bytesUsedNewTotal
|
||||
|
||||
const result = await this.setSubscriptonSettingValue.execute({
|
||||
userSubscriptionUuid: subscription.uuid,
|
||||
settingName: SettingName.NAMES.FileUploadBytesUsed,
|
||||
value: (+bytesAlreadyUsed + bytesUsed).toString(),
|
||||
value: bytesUsedValue.toString(),
|
||||
})
|
||||
|
||||
/* istanbul ignore next */
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>,
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.22.5...@standardnotes/domain-events-infra@1.22.6) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.22.4",
|
||||
"version": "1.22.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.139.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.138.2...@standardnotes/domain-events@2.139.0) (2023-12-14)
|
||||
|
||||
### Features
|
||||
|
||||
* add procedure for recalculating file quota for user ([#980](https://github.com/standardnotes/server/issues/980)) ([de4fcf9](https://github.com/standardnotes/server/commit/de4fcf9a4c308ad7d71c42fe5c27af18b8614e1a))
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.138.1",
|
||||
"version": "2.139.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -12,4 +12,5 @@ export interface EmailRequestedEventPayload {
|
||||
attachmentFileName: string
|
||||
attachmentContentType: string
|
||||
}>
|
||||
userUuid?: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { FileQuotaRecalculatedEventPayload } from './FileQuotaRecalculatedEventPayload'
|
||||
|
||||
export interface FileQuotaRecalculatedEvent extends DomainEventInterface {
|
||||
type: 'FILE_QUOTA_RECALCULATED'
|
||||
payload: FileQuotaRecalculatedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface FileQuotaRecalculatedEventPayload {
|
||||
userUuid: string
|
||||
totalFileByteSize: number
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from './DomainEventInterface'
|
||||
import { FileQuotaRecalculationRequestedEventPayload } from './FileQuotaRecalculationRequestedEventPayload'
|
||||
|
||||
export interface FileQuotaRecalculationRequestedEvent extends DomainEventInterface {
|
||||
type: 'FILE_QUOTA_RECALCULATION_REQUESTED'
|
||||
payload: FileQuotaRecalculationRequestedEventPayload
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface FileQuotaRecalculationRequestedEventPayload {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -30,6 +30,10 @@ export * from './Event/ExitDiscountWithdrawRequestedEvent'
|
||||
export * from './Event/ExitDiscountWithdrawRequestedEventPayload'
|
||||
export * from './Event/ExtensionKeyGrantedEvent'
|
||||
export * from './Event/ExtensionKeyGrantedEventPayload'
|
||||
export * from './Event/FileQuotaRecalculatedEvent'
|
||||
export * from './Event/FileQuotaRecalculatedEventPayload'
|
||||
export * from './Event/FileQuotaRecalculationRequestedEvent'
|
||||
export * from './Event/FileQuotaRecalculationRequestedEventPayload'
|
||||
export * from './Event/FileRemovedEvent'
|
||||
export * from './Event/FileRemovedEventPayload'
|
||||
export * from './Event/FileUploadedEvent'
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.37.0](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.36.6...@standardnotes/files-server@1.37.0) (2023-12-14)
|
||||
|
||||
### Features
|
||||
|
||||
* add procedure for recalculating file quota for user ([#980](https://github.com/standardnotes/server/issues/980)) ([de4fcf9](https://github.com/standardnotes/server/commit/de4fcf9a4c308ad7d71c42fe5c27af18b8614e1a))
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@ COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-web' )
|
||||
echo "Starting Web..."
|
||||
exec node docker/entrypoint-server.js
|
||||
;;
|
||||
|
||||
'start-worker' )
|
||||
echo "Starting Worker..."
|
||||
exec node docker/entrypoint-worker.js
|
||||
;;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.36.4",
|
||||
"version": "1.37.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -52,8 +52,12 @@ import { S3FileMover } from '../Infra/S3/S3FileMover'
|
||||
import { FSFileMover } from '../Infra/FS/FSFileMover'
|
||||
import { MoveFile } from '../Domain/UseCase/MoveFile/MoveFile'
|
||||
import { SharedVaultValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/SharedVaultValetTokenAuthMiddleware'
|
||||
import { RecalculateQuota } from '../Domain/UseCase/RecalculateQuota/RecalculateQuota'
|
||||
import { FileQuotaRecalculationRequestedEventHandler } from '../Domain/Handler/FileQuotaRecalculationRequestedEventHandler'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
|
||||
async load(configuration?: {
|
||||
directCallDomainEventPublisher?: DirectCallDomainEventPublisher
|
||||
logger?: Transform
|
||||
@@ -242,6 +246,15 @@ export class ContainerConfigLoader {
|
||||
),
|
||||
)
|
||||
container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
|
||||
container
|
||||
.bind<RecalculateQuota>(TYPES.Files_RecalculateQuota)
|
||||
.toConstantValue(
|
||||
new RecalculateQuota(
|
||||
container.get<FileDownloaderInterface>(TYPES.Files_FileDownloader),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory),
|
||||
),
|
||||
)
|
||||
|
||||
// middleware
|
||||
container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
|
||||
@@ -272,6 +285,14 @@ export class ContainerConfigLoader {
|
||||
container.get<winston.Logger>(TYPES.Files_Logger),
|
||||
),
|
||||
)
|
||||
container
|
||||
.bind<FileQuotaRecalculationRequestedEventHandler>(TYPES.Files_FileQuotaRecalculationRequestedEventHandler)
|
||||
.toConstantValue(
|
||||
new FileQuotaRecalculationRequestedEventHandler(
|
||||
container.get<RecalculateQuota>(TYPES.Files_RecalculateQuota),
|
||||
container.get<winston.Logger>(TYPES.Files_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Files_AccountDeletionRequestedEventHandler)],
|
||||
@@ -279,6 +300,12 @@ export class ContainerConfigLoader {
|
||||
'SHARED_SUBSCRIPTION_INVITATION_CANCELED',
|
||||
container.get(TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler),
|
||||
],
|
||||
[
|
||||
'FILE_QUOTA_RECALCULATION_REQUESTED',
|
||||
container.get<FileQuotaRecalculationRequestedEventHandler>(
|
||||
TYPES.Files_FileQuotaRecalculationRequestedEventHandler,
|
||||
),
|
||||
],
|
||||
])
|
||||
|
||||
if (isConfiguredForHomeServer) {
|
||||
@@ -316,7 +343,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}` },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const TYPES = {
|
||||
Files_RemoveFile: Symbol.for('Files_RemoveFile'),
|
||||
Files_MoveFile: Symbol.for('Files_MoveFile'),
|
||||
Files_MarkFilesToBeRemoved: Symbol.for('Files_MarkFilesToBeRemoved'),
|
||||
Files_RecalculateQuota: Symbol.for('Files_RecalculateQuota'),
|
||||
|
||||
// services
|
||||
Files_ValetTokenDecoder: Symbol.for('Files_ValetTokenDecoder'),
|
||||
@@ -57,6 +58,7 @@ const TYPES = {
|
||||
Files_SharedSubscriptionInvitationCanceledEventHandler: Symbol.for(
|
||||
'Files_SharedSubscriptionInvitationCanceledEventHandler',
|
||||
),
|
||||
Files_FileQuotaRecalculationRequestedEventHandler: Symbol.for('Files_FileQuotaRecalculationRequestedEventHandler'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileMovedEvent,
|
||||
FileQuotaRecalculatedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@@ -13,6 +14,24 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(private timer: TimerInterface) {}
|
||||
|
||||
createFileQuotaRecalculatedEvent(payload: {
|
||||
userUuid: string
|
||||
totalFileByteSize: number
|
||||
}): FileQuotaRecalculatedEvent {
|
||||
return {
|
||||
type: 'FILE_QUOTA_RECALCULATED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: payload.userUuid,
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.Files,
|
||||
},
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
createFileRemovedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
SharedVaultFileRemovedEvent,
|
||||
SharedVaultFileUploadedEvent,
|
||||
SharedVaultFileMovedEvent,
|
||||
FileQuotaRecalculatedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createFileQuotaRecalculatedEvent(payload: { userUuid: string; totalFileByteSize: number }): FileQuotaRecalculatedEvent
|
||||
createFileUploadedEvent(payload: {
|
||||
userUuid: string
|
||||
filePath: string
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { DomainEventHandlerInterface, FileQuotaRecalculationRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { RecalculateQuota } from '../UseCase/RecalculateQuota/RecalculateQuota'
|
||||
|
||||
export class FileQuotaRecalculationRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
private recalculateQuota: RecalculateQuota,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: FileQuotaRecalculationRequestedEvent): Promise<void> {
|
||||
this.logger.info('Recalculating quota for user...', {
|
||||
userId: event.payload.userUuid,
|
||||
})
|
||||
|
||||
const result = await this.recalculateQuota.execute({
|
||||
userUuid: event.payload.userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error('Could not recalculate quota', {
|
||||
userId: event.payload.userUuid,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info('Quota recalculated', {
|
||||
userId: event.payload.userUuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,5 @@ import { Readable } from 'stream'
|
||||
export interface FileDownloaderInterface {
|
||||
createDownloadStream(filePath: string, startRange: number, endRange: number): Promise<Readable>
|
||||
getFileSize(filePath: string): Promise<number>
|
||||
listFiles(userUuid: string): Promise<{ name: string; size: number }[]>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { DomainEventPublisherInterface, FileQuotaRecalculatedEvent } from '@standardnotes/domain-events'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
import { RecalculateQuota } from './RecalculateQuota'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
|
||||
describe('RecalculateQuota', () => {
|
||||
let fileDownloader: FileDownloaderInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
|
||||
const createUseCase = () => new RecalculateQuota(fileDownloader, domainEventPublisher, domainEventFactory)
|
||||
|
||||
beforeEach(() => {
|
||||
fileDownloader = {} as jest.Mocked<FileDownloaderInterface>
|
||||
fileDownloader.listFiles = jest.fn().mockResolvedValue([
|
||||
{
|
||||
name: 'test-file',
|
||||
size: 123,
|
||||
},
|
||||
])
|
||||
|
||||
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
|
||||
domainEventPublisher.publish = jest.fn()
|
||||
|
||||
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
|
||||
domainEventFactory.createFileQuotaRecalculatedEvent = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<FileQuotaRecalculatedEvent>)
|
||||
})
|
||||
|
||||
it('publishes a file quota recalculated event', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
expect(domainEventFactory.createFileQuotaRecalculatedEvent).toHaveBeenCalledWith({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
totalFileByteSize: 123,
|
||||
})
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns a failure result if user uuid is invalid', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
userUuid: 'invalid-user-uuid',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
|
||||
import { RecalculateQuotaDTO } from './RecalculateQuotaDTO'
|
||||
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
|
||||
import { FileDownloaderInterface } from '../../Services/FileDownloaderInterface'
|
||||
|
||||
export class RecalculateQuota implements UseCaseInterface<void> {
|
||||
constructor(
|
||||
private fileDownloader: FileDownloaderInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: RecalculateQuotaDTO): Promise<Result<void>> {
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const filesList = await this.fileDownloader.listFiles(userUuid.value)
|
||||
let totalFileByteSize = 0
|
||||
for (const file of filesList) {
|
||||
totalFileByteSize += file.size
|
||||
}
|
||||
|
||||
const event = this.domainEventFactory.createFileQuotaRecalculatedEvent({
|
||||
userUuid: dto.userUuid,
|
||||
totalFileByteSize,
|
||||
})
|
||||
|
||||
await this.domainEventPublisher.publish(event)
|
||||
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface RecalculateQuotaDTO {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -9,6 +9,21 @@ import TYPES from '../../Bootstrap/Types'
|
||||
export class FSFileDownloader implements FileDownloaderInterface {
|
||||
constructor(@inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string) {}
|
||||
|
||||
async listFiles(userUuid: string): Promise<{ name: string; size: number }[]> {
|
||||
const filesList = []
|
||||
|
||||
const files = await promises.readdir(`${this.fileUploadPath}/${userUuid}`)
|
||||
for (const file of files) {
|
||||
const fileStat = await promises.stat(`${this.fileUploadPath}/${userUuid}/${file}`)
|
||||
filesList.push({
|
||||
name: file,
|
||||
size: fileStat.size,
|
||||
})
|
||||
}
|
||||
|
||||
return filesList
|
||||
}
|
||||
|
||||
async getFileSize(filePath: string): Promise<number> {
|
||||
return (await promises.stat(`${this.fileUploadPath}/${filePath}`)).size
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetObjectCommand, HeadObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import { GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
@@ -34,4 +34,25 @@ export class S3FileDownloader implements FileDownloaderInterface {
|
||||
|
||||
return head.ContentLength as number
|
||||
}
|
||||
|
||||
async listFiles(userUuid: string): Promise<{ name: string; size: number }[]> {
|
||||
const objectsList = await this.s3Client.send(
|
||||
new ListObjectsV2Command({
|
||||
Bucket: `${this.s3BuckeName}/${userUuid}/`,
|
||||
}),
|
||||
)
|
||||
|
||||
const filesList = []
|
||||
for (const object of objectsList.Contents ?? []) {
|
||||
if (!object.Key) {
|
||||
continue
|
||||
}
|
||||
filesList.push({
|
||||
name: object.Key,
|
||||
size: object.Size ?? 0,
|
||||
})
|
||||
}
|
||||
|
||||
return filesList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [1.3.1](https://github.com/standardnotes/server/compare/@standardnotes/grpc@1.3.0...@standardnotes/grpc@1.3.1) (2023-11-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
39
packages/grpc/lib/auth_grpc_pb.d.ts
vendored
39
packages/grpc/lib/auth_grpc_pb.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
40
packages/grpc/lib/auth_pb.d.ts
vendored
40
packages/grpc/lib/auth_pb.d.ts
vendored
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/grpc",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -27,7 +27,7 @@
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.9.11",
|
||||
"@grpc/grpc-js": "^1.9.12",
|
||||
"google-protobuf": "^3.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.21](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.20...@standardnotes/home-server@1.22.21) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [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
|
||||
|
||||
## [1.22.10](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.9...@standardnotes/home-server@1.22.10) (2023-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.9](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.8...@standardnotes/home-server@1.22.9) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.8](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.7...@standardnotes/home-server@1.22.8) (2023-12-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.22.8",
|
||||
"version": "1.22.21",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.51.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.51.6...@standardnotes/revisions-server@1.51.7) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.51.4",
|
||||
"version": "1.51.7",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.27.12](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.27.11...@standardnotes/scheduler-server@1.27.12) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.27.9",
|
||||
"version": "1.27.12",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -3,6 +3,49 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.129.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.129.0...@standardnotes/syncing-server@1.129.1) (2023-12-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
# [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
|
||||
|
||||
## [1.127.6](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.127.5...@standardnotes/syncing-server@1.127.6) (2023-12-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.127.6",
|
||||
"version": "1.129.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"@aws-sdk/client-s3": "^3.462.0",
|
||||
"@aws-sdk/client-sns": "^3.462.0",
|
||||
"@aws-sdk/client-sqs": "^3.462.0",
|
||||
"@grpc/grpc-js": "^1.9.11",
|
||||
"@grpc/grpc-js": "^1.9.12",
|
||||
"@standardnotes/api": "^1.26.26",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -329,6 +329,7 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
attachmentFileName: string
|
||||
attachmentContentType: string
|
||||
}>
|
||||
userUuid?: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_REQUESTED',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ export interface SaveItemsDTO {
|
||||
readOnlyAccess: boolean
|
||||
sessionUuid: string | null
|
||||
snjsVersion: string
|
||||
isFreeUser: boolean
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DomainEventInterface } from '@standardnotes/domain-events'
|
||||
|
||||
export interface SendEventToClientsDTO {
|
||||
sharedVaultUuid: string
|
||||
event: DomainEventInterface
|
||||
originatingUserUuid: string
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user