mirror of
https://github.com/standardnotes/server
synced 2026-01-22 02:04:30 -05:00
Compare commits
44 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c00c7becae | ||
|
|
89dc6c19bf | ||
|
|
972a91d59f | ||
|
|
045358ddbf | ||
|
|
c7217a92ba | ||
|
|
3da7a21cde | ||
|
|
351e18f638 | ||
|
|
4f2129c4e0 | ||
|
|
d7a1c667dd | ||
|
|
4de0bfa36d | ||
|
|
0443de88ce | ||
|
|
f830bac873 | ||
|
|
517ae5ded9 | ||
|
|
6062f85000 | ||
|
|
e2205c3849 | ||
|
|
0b46eff16e | ||
|
|
df67982bca | ||
|
|
d44866b3c0 | ||
|
|
6ee18bffe6 | ||
|
|
a881dd2d79 | ||
|
|
b767e1f072 | ||
|
|
e3cb1faba4 | ||
|
|
5c5f988055 | ||
|
|
c7d2adf091 | ||
|
|
a4ad37f309 | ||
|
|
73c2cc1222 | ||
|
|
9380900aaf | ||
|
|
02f4d5c717 | ||
|
|
1f4b26d269 | ||
|
|
e253825da6 | ||
|
|
2bddd947ba | ||
|
|
b7173346d2 | ||
|
|
01641975c0 | ||
|
|
7abd80cdba | ||
|
|
aeb5ea1874 | ||
|
|
d2a371b92c | ||
|
|
3ea3b24bb6 | ||
|
|
0c3bc0cae6 | ||
|
|
d56410984a | ||
|
|
4dd2eb9349 | ||
|
|
709aec5eeb | ||
|
|
f1aa431c22 | ||
|
|
86d0e565ed | ||
|
|
92bb447cac |
2
.github/ci.env
vendored
2
.github/ci.env
vendored
@@ -27,4 +27,4 @@ AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
|
||||
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
|
||||
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
|
||||
|
||||
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=1000000
|
||||
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=100000
|
||||
|
||||
2
.github/workflows/e2e-home-server.yml
vendored
2
.github/workflows/e2e-home-server.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
|
||||
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
|
||||
echo "REVISIONS_FREQUENCY=2" >> packages/home-server/.env
|
||||
echo "CONTENT_SIZE_TRANSFER_LIMIT=1000000" >> packages/home-server/.env
|
||||
echo "CONTENT_SIZE_TRANSFER_LIMIT=100000" >> packages/home-server/.env
|
||||
echo "DB_HOST=localhost" >> packages/home-server/.env
|
||||
echo "DB_PORT=3306" >> packages/home-server/.env
|
||||
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.89.19](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.18...@standardnotes/api-gateway@1.89.19) (2024-01-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
|
||||
|
||||
## [1.89.18](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.17...@standardnotes/api-gateway@1.89.18) (2024-01-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.89.17](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.16...@standardnotes/api-gateway@1.89.17) (2024-01-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** disable http call retries ([7abd80c](https://github.com/standardnotes/server/commit/7abd80cdbaba53840f632d418bd557b35b722699))
|
||||
|
||||
## [1.89.16](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.15...@standardnotes/api-gateway@1.89.16) (2024-01-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** disable sync request retries ([d2a371b](https://github.com/standardnotes/server/commit/d2a371b92c8b2b7f8921fe57f162e74d4944715d))
|
||||
|
||||
## [1.89.15](https://github.com/standardnotes/server/compare/@standardnotes/api-gateway@1.89.14...@standardnotes/api-gateway@1.89.15) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -36,12 +36,17 @@ 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 { ResponseLocals } from '../src/Controller/ResponseLocals'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
|
||||
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
|
||||
: '50mb'
|
||||
|
||||
const server = new InversifyExpressServer(container)
|
||||
|
||||
server.setConfig((app) => {
|
||||
@@ -72,7 +77,7 @@ void container.load().then((container) => {
|
||||
}),
|
||||
)
|
||||
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(json({ limit: requestPayloadLimit }))
|
||||
app.use(
|
||||
text({
|
||||
type: ['text/plain', 'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded; charset=utf-8'],
|
||||
@@ -91,12 +96,14 @@ void container.load().then((container) => {
|
||||
|
||||
server.setErrorConfig((app) => {
|
||||
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
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,
|
||||
userId: locals.user ? locals.user.uuid : undefined,
|
||||
})
|
||||
logger.debug(
|
||||
`[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${
|
||||
@@ -104,6 +111,16 @@ void container.load().then((container) => {
|
||||
}] Request body: ${JSON.stringify(request.body)}`,
|
||||
)
|
||||
|
||||
if ('type' in error && error.type === 'entity.too.large') {
|
||||
response.status(413).send({
|
||||
error: {
|
||||
message: 'The request payload is too large.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.status(500).send({
|
||||
error: {
|
||||
message:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.89.15",
|
||||
"version": "1.89.19",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Logger } from 'winston'
|
||||
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
|
||||
import { ResponseLocals } from './ResponseLocals'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@@ -55,33 +57,27 @@ export abstract class AuthMiddleware extends BaseMiddleware {
|
||||
crossServiceTokenFetchedFromCache = false
|
||||
}
|
||||
|
||||
response.locals.authToken = crossServiceToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>(
|
||||
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
|
||||
await this.crossServiceTokenCache.set({
|
||||
key: cacheKey,
|
||||
encodedCrossServiceToken: response.locals.authToken,
|
||||
encodedCrossServiceToken: crossServiceToken,
|
||||
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
|
||||
userUuid: decodedToken.user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.session = decodedToken.session
|
||||
response.locals.roles = decodedToken.roles
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
|
||||
if (response.locals.readOnlyAccess) {
|
||||
this.logger.debug('User operates on read-only access', {
|
||||
codeTag: 'AuthMiddleware',
|
||||
userId: response.locals.user.uuid,
|
||||
})
|
||||
}
|
||||
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
|
||||
Object.assign(response.locals, {
|
||||
authToken: crossServiceToken,
|
||||
user: decodedToken.user,
|
||||
session: decodedToken.session,
|
||||
roles: decodedToken.roles,
|
||||
sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
|
||||
readOnlyAccess: decodedToken.session?.readonly_access ?? false,
|
||||
isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
|
||||
belongsToSharedVaults: decodedToken.belongs_to_shared_vaults ?? [],
|
||||
} as ResponseLocals)
|
||||
} catch (error) {
|
||||
let detailedErrorMessage = (error as Error).message
|
||||
if (error instanceof AxiosError) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { verify } from 'jsonwebtoken'
|
||||
import { Logger } from 'winston'
|
||||
import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { ResponseLocals } from './ResponseLocals'
|
||||
|
||||
export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@@ -90,15 +91,16 @@ export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
|
||||
|
||||
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
|
||||
Object.assign(response.locals, {
|
||||
authToken: crossServiceToken,
|
||||
user: decodedToken.user,
|
||||
session: decodedToken.session,
|
||||
roles: decodedToken.roles,
|
||||
isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
|
||||
readOnlyAccess: decodedToken.session?.readonly_access ?? false,
|
||||
} as ResponseLocals)
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Could not pass the request to websocket connection validation on underlying service: ${
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface OfflineResponseLocals {
|
||||
offlineAuthToken: string
|
||||
userEmail: string
|
||||
featuresToken: string
|
||||
}
|
||||
29
packages/api-gateway/src/Controller/ResponseLocals.ts
Normal file
29
packages/api-gateway/src/Controller/ResponseLocals.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Role } from '@standardnotes/security'
|
||||
|
||||
export interface ResponseLocals {
|
||||
authToken: string
|
||||
user: {
|
||||
uuid: string
|
||||
email: string
|
||||
}
|
||||
roles: Array<Role>
|
||||
session?: {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
readOnlyAccess: boolean
|
||||
isFreeUser: boolean
|
||||
belongsToSharedVaults?: Array<{
|
||||
shared_vault_uuid: string
|
||||
permission: string
|
||||
}>
|
||||
sharedVaultOwnerContext?: {
|
||||
upload_bytes_limit: number
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
|
||||
|
||||
export interface SubscriptionResponseLocals {
|
||||
tokenAuthenticationMethod: TokenAuthenticationMethod
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
|
||||
import { Logger } from 'winston'
|
||||
import { TYPES } from '../Bootstrap/Types'
|
||||
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
|
||||
import { ResponseLocals } from './ResponseLocals'
|
||||
import { OfflineResponseLocals } from './OfflineResponseLocals'
|
||||
import { SubscriptionResponseLocals } from './SubscriptionResponseLocals'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
|
||||
@@ -34,13 +37,16 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.tokenAuthenticationMethod = email
|
||||
? TokenAuthenticationMethod.OfflineSubscriptionToken
|
||||
: TokenAuthenticationMethod.SubscriptionToken
|
||||
const locals = {
|
||||
tokenAuthenticationMethod: email
|
||||
? TokenAuthenticationMethod.OfflineSubscriptionToken
|
||||
: TokenAuthenticationMethod.SubscriptionToken,
|
||||
} as SubscriptionResponseLocals
|
||||
Object.assign(response.locals, locals)
|
||||
|
||||
try {
|
||||
const url =
|
||||
response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
|
||||
locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
|
||||
? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate`
|
||||
: `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate`
|
||||
|
||||
@@ -65,7 +71,7 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
|
||||
if (locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
|
||||
this.handleOfflineAuthTokenValidationResponse(response, authResponse)
|
||||
|
||||
return next()
|
||||
@@ -101,24 +107,26 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
|
||||
}
|
||||
|
||||
private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
|
||||
response.locals.offlineAuthToken = authResponse.data.authToken
|
||||
|
||||
const decodedToken = <OfflineUserTokenData>(
|
||||
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
|
||||
response.locals.offlineUserEmail = decodedToken.userEmail
|
||||
response.locals.offlineFeaturesToken = decodedToken.featuresToken
|
||||
Object.assign(response.locals, {
|
||||
offlineAuthToken: authResponse.data.authToken,
|
||||
userEmail: decodedToken.userEmail,
|
||||
featuresToken: decodedToken.featuresToken,
|
||||
} as OfflineResponseLocals)
|
||||
}
|
||||
|
||||
private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
|
||||
response.locals.authToken = authResponse.data.authToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>(
|
||||
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
)
|
||||
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.roles = decodedToken.roles
|
||||
Object.assign(response.locals, {
|
||||
authToken: authResponse.data.authToken,
|
||||
user: decodedToken.user,
|
||||
roles: decodedToken.roles,
|
||||
} as ResponseLocals)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AxiosError, AxiosInstance } from 'axios'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { TYPES } from '../Bootstrap/Types'
|
||||
import { ResponseLocals } from './ResponseLocals'
|
||||
|
||||
@injectable()
|
||||
export class WebSocketAuthMiddleware extends BaseMiddleware {
|
||||
@@ -55,13 +56,14 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
|
||||
|
||||
const crossServiceToken = authResponse.data.authToken
|
||||
|
||||
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
|
||||
Object.assign(response.locals, {
|
||||
authToken: crossServiceToken,
|
||||
user: decodedToken.user,
|
||||
session: decodedToken.session,
|
||||
roles: decodedToken.roles,
|
||||
} as ResponseLocals)
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
|
||||
@@ -16,6 +16,8 @@ import { TYPES } from '../../Bootstrap/Types'
|
||||
import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
|
||||
import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
|
||||
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
import { SubscriptionResponseLocals } from '../SubscriptionResponseLocals'
|
||||
|
||||
@controller('/v1/users')
|
||||
export class UsersController extends BaseHttpController {
|
||||
@@ -214,7 +216,9 @@ export class UsersController extends BaseHttpController {
|
||||
|
||||
@httpGet('/subscription', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
|
||||
async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise<void> {
|
||||
if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
|
||||
const locals = response.locals as SubscriptionResponseLocals & ResponseLocals
|
||||
|
||||
if (locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
@@ -227,11 +231,7 @@ export class UsersController extends BaseHttpController {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier(
|
||||
'GET',
|
||||
'users/:userUuid/subscription',
|
||||
response.locals.user.uuid,
|
||||
),
|
||||
this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/:userUuid/subscription', locals.user.uuid),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Request, Response } from 'express'
|
||||
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
|
||||
import { ResponseLocals } from '../../Controller/ResponseLocals'
|
||||
|
||||
export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
constructor(
|
||||
@@ -134,11 +135,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
|
||||
response: Response,
|
||||
serviceResponse: { statusCode: number; json: Record<string, unknown> },
|
||||
): void {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
void response.status(serviceResponse.statusCode).send({
|
||||
meta: {
|
||||
auth: {
|
||||
userUuid: response.locals.user?.uuid,
|
||||
roles: response.locals.roles,
|
||||
userUuid: locals.user?.uuid,
|
||||
roles: locals.roles,
|
||||
},
|
||||
server: {
|
||||
filesServerUrl: this.filesServerUrl,
|
||||
|
||||
@@ -8,6 +8,8 @@ import { TYPES } from '../../Bootstrap/Types'
|
||||
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
|
||||
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { ResponseLocals } from '../../Controller/ResponseLocals'
|
||||
import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals'
|
||||
|
||||
@injectable()
|
||||
export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
@@ -175,8 +177,9 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
retryAttempt?: number,
|
||||
): Promise<AxiosResponse | undefined> {
|
||||
const locals = response.locals as ResponseLocals | OfflineResponseLocals
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
for (const headerName of Object.keys(request.headers)) {
|
||||
@@ -186,12 +189,12 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
delete headers.host
|
||||
delete headers['content-length']
|
||||
|
||||
if (response.locals.authToken) {
|
||||
headers['X-Auth-Token'] = response.locals.authToken
|
||||
if ('authToken' in locals && locals.authToken) {
|
||||
headers['X-Auth-Token'] = locals.authToken
|
||||
}
|
||||
|
||||
if (response.locals.offlineAuthToken) {
|
||||
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
|
||||
if ('offlineAuthToken' in locals && locals.offlineAuthToken) {
|
||||
headers['X-Auth-Offline-Token'] = locals.offlineAuthToken
|
||||
}
|
||||
|
||||
const serviceResponse = await this.httpClient.request({
|
||||
@@ -213,35 +216,17 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
await this.crossServiceTokenCache.invalidate(userUuid)
|
||||
}
|
||||
|
||||
if (retryAttempt) {
|
||||
this.logger.debug(`Request to ${serverUrl}/${endpoint} succeeded after ${retryAttempt} retries`)
|
||||
}
|
||||
|
||||
return serviceResponse
|
||||
} catch (error) {
|
||||
const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
|
||||
this.logger.debug(`Retrying request to ${serverUrl}/${endpoint} for the ${nextRetryAttempt} time`)
|
||||
|
||||
return this.getServerResponse(serverUrl, request, response, endpoint, payload, nextRetryAttempt)
|
||||
}
|
||||
|
||||
let detailedErrorMessage = (error as Error).message
|
||||
if (error instanceof AxiosError) {
|
||||
detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
tooManyRetryAttempts
|
||||
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries`
|
||||
: `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
|
||||
`Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`,
|
||||
{
|
||||
userId: response.locals.user ? response.locals.user.uuid : undefined,
|
||||
userId: (locals as ResponseLocals).user ? (locals as ResponseLocals).user.uuid : undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -276,6 +261,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
|
||||
|
||||
if (!serviceResponse) {
|
||||
@@ -293,8 +279,8 @@ export class HttpServiceProxy implements ServiceProxyInterface {
|
||||
response.status(serviceResponse.status).send({
|
||||
meta: {
|
||||
auth: {
|
||||
userUuid: response.locals.user?.uuid,
|
||||
roles: response.locals.roles,
|
||||
userUuid: locals.user?.uuid,
|
||||
roles: locals.roles,
|
||||
},
|
||||
server: {
|
||||
filesServerUrl: this.filesServerUrl,
|
||||
|
||||
@@ -40,7 +40,6 @@ export class EndpointResolver implements EndpointResolverInterface {
|
||||
// Tokens Controller
|
||||
['[POST]:subscription-tokens', 'auth.subscription-tokens.create'],
|
||||
// Users Controller
|
||||
['[PATCH]:users/:userId', 'auth.users.update'],
|
||||
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
|
||||
['[DELETE]:users/:userUuid', 'auth.users.delete'],
|
||||
['[POST]:listed', 'auth.users.createListedAccount'],
|
||||
|
||||
@@ -9,6 +9,8 @@ import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCache
|
||||
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
|
||||
import { GRPCSyncingServerServiceProxy } from './GRPCSyncingServerServiceProxy'
|
||||
import { Status } from '@grpc/grpc-js/build/src/constants'
|
||||
import { ResponseLocals } from '../../Controller/ResponseLocals'
|
||||
import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals'
|
||||
|
||||
export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
constructor(
|
||||
@@ -134,48 +136,23 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
request: Request,
|
||||
response: Response,
|
||||
payload?: Record<string, unknown> | string,
|
||||
retryAttempt?: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload)
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
response.status(result.status).send({
|
||||
meta: {
|
||||
auth: {
|
||||
userUuid: response.locals.user?.uuid,
|
||||
roles: response.locals.roles,
|
||||
},
|
||||
server: {
|
||||
filesServerUrl: this.filesServerUrl,
|
||||
},
|
||||
const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload)
|
||||
|
||||
response.status(result.status).send({
|
||||
meta: {
|
||||
auth: {
|
||||
userUuid: locals.user?.uuid,
|
||||
roles: locals.roles,
|
||||
},
|
||||
data: result.data,
|
||||
})
|
||||
|
||||
if (retryAttempt) {
|
||||
this.logger.debug(`Request to Syncing Server succeeded after ${retryAttempt} retries`, {
|
||||
userId: response.locals.user ? response.locals.user.uuid : undefined,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
const requestDidNotMakeIt =
|
||||
'code' in (error as Record<string, unknown>) && (error as Record<string, unknown>).code === Status.UNAVAILABLE
|
||||
|
||||
const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
|
||||
if (!tooManyRetryAttempts && requestDidNotMakeIt) {
|
||||
await this.timer.sleep(50)
|
||||
|
||||
const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
|
||||
|
||||
this.logger.debug(`Retrying request to Syncing Server for the ${nextRetryAttempt} time`, {
|
||||
userId: response.locals.user ? response.locals.user.uuid : undefined,
|
||||
})
|
||||
|
||||
return this.callSyncingServerGRPC(request, response, payload, nextRetryAttempt)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
server: {
|
||||
filesServerUrl: this.filesServerUrl,
|
||||
},
|
||||
},
|
||||
data: result.data,
|
||||
})
|
||||
}
|
||||
|
||||
async callRevisionsServer(
|
||||
@@ -277,6 +254,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
payload?: Record<string, unknown> | string,
|
||||
retryAttempt?: number,
|
||||
): Promise<AxiosResponse | undefined> {
|
||||
const locals = response.locals as ResponseLocals | OfflineResponseLocals
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
for (const headerName of Object.keys(request.headers)) {
|
||||
@@ -286,12 +265,12 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
delete headers.host
|
||||
delete headers['content-length']
|
||||
|
||||
if (response.locals.authToken) {
|
||||
headers['X-Auth-Token'] = response.locals.authToken
|
||||
if ('authToken' in locals && locals.authToken) {
|
||||
headers['X-Auth-Token'] = locals.authToken
|
||||
}
|
||||
|
||||
if (response.locals.offlineAuthToken) {
|
||||
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
|
||||
if ('offlineAuthToken' in locals && locals.offlineAuthToken) {
|
||||
headers['X-Auth-Offline-Token'] = locals.offlineAuthToken
|
||||
}
|
||||
|
||||
const serviceResponse = await this.httpClient.request({
|
||||
@@ -341,7 +320,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
? `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,
|
||||
userId: (locals as ResponseLocals).user ? (locals as ResponseLocals).user.uuid : undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -376,6 +355,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
endpoint: string,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<void> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
|
||||
|
||||
if (!serviceResponse) {
|
||||
@@ -393,8 +374,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
|
||||
response.status(serviceResponse.status).send({
|
||||
meta: {
|
||||
auth: {
|
||||
userUuid: response.locals.user?.uuid,
|
||||
roles: response.locals.roles,
|
||||
userUuid: locals.user?.uuid,
|
||||
roles: locals.roles,
|
||||
},
|
||||
server: {
|
||||
filesServerUrl: this.filesServerUrl,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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'
|
||||
import { ResponseLocals } from '../../Controller/ResponseLocals'
|
||||
|
||||
export class GRPCSyncingServerServiceProxy {
|
||||
constructor(
|
||||
@@ -20,24 +21,26 @@ export class GRPCSyncingServerServiceProxy {
|
||||
response: Response,
|
||||
payload?: Record<string, unknown> | string,
|
||||
): Promise<{ status: number; data: unknown }> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const syncRequest = this.syncRequestGRPCMapper.toProjection(payload as Record<string, unknown>)
|
||||
|
||||
const metadata = new Metadata()
|
||||
metadata.set('x-user-uuid', response.locals.user.uuid)
|
||||
metadata.set('x-user-uuid', locals.user.uuid)
|
||||
metadata.set('x-snjs-version', request.headers['x-snjs-version'] as string)
|
||||
metadata.set('x-read-only-access', response.locals.readOnlyAccess ? 'true' : 'false')
|
||||
if (response.locals.readOnlyAccess) {
|
||||
metadata.set('x-read-only-access', locals.readOnlyAccess ? 'true' : 'false')
|
||||
if (locals.readOnlyAccess) {
|
||||
this.logger.debug('Syncing with read-only access', {
|
||||
codeTag: 'GRPCSyncingServerServiceProxy',
|
||||
userId: response.locals.user.uuid,
|
||||
userId: locals.user.uuid,
|
||||
})
|
||||
}
|
||||
if (response.locals.session) {
|
||||
metadata.set('x-session-uuid', response.locals.session.uuid)
|
||||
if (locals.session) {
|
||||
metadata.set('x-session-uuid', locals.session.uuid)
|
||||
}
|
||||
metadata.set('x-is-free-user', response.locals.isFreeUser ? 'true' : 'false')
|
||||
metadata.set('x-is-free-user', locals.isFreeUser ? 'true' : 'false')
|
||||
|
||||
this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
|
||||
if (error) {
|
||||
@@ -52,7 +55,7 @@ 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,
|
||||
userId: locals.user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ export class GRPCSyncingServerServiceProxy {
|
||||
) {
|
||||
this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, {
|
||||
codeTag: 'GRPCSyncingServerServiceProxy.catch',
|
||||
userId: response.locals.user.uuid,
|
||||
userId: locals.user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,35 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.177.18](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.17...@standardnotes/auth-server@1.177.18) (2024-01-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add more logs to syncing subscription ([c7217a9](https://github.com/standardnotes/server/commit/c7217a92ba89d8b5f4963a832aa7561dd146ca0d))
|
||||
* **auth:** add renewal for shared offline subscriptions ([045358d](https://github.com/standardnotes/server/commit/045358ddbf300996a23bba8d6945b1d7b5f6e862))
|
||||
|
||||
## [1.177.17](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.16...@standardnotes/auth-server@1.177.17) (2024-01-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add debug logs for subscription sync requested event ([351e18f](https://github.com/standardnotes/server/commit/351e18f6389c2dbaa2107e6549be9928c2e8834f))
|
||||
|
||||
## [1.177.16](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.15...@standardnotes/auth-server@1.177.16) (2024-01-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** update shared subscriptions upon subscription sync ([#1022](https://github.com/standardnotes/server/issues/1022)) ([d7a1c66](https://github.com/standardnotes/server/commit/d7a1c667dd62dacc1ef15f2a4f408dc07045fcad))
|
||||
|
||||
## [1.177.15](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.14...@standardnotes/auth-server@1.177.15) (2024-01-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** check for user agent persisting on session during a session refresh ([#1016](https://github.com/standardnotes/server/issues/1016)) ([0b46eff](https://github.com/standardnotes/server/commit/0b46eff16ea0c32cac91ead04474303500359f4f))
|
||||
|
||||
## [1.177.14](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.13...@standardnotes/auth-server@1.177.14) (2024-01-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.177.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.12...@standardnotes/auth-server@1.177.13) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -35,6 +35,7 @@ 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'
|
||||
import { ResponseLocals } from '../src/Infra/InversifyExpressUtils/ResponseLocals'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
@@ -59,12 +60,13 @@ void container.load().then((container) => {
|
||||
|
||||
server.setErrorConfig((app) => {
|
||||
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
|
||||
const locals = response.locals as ResponseLocals
|
||||
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,
|
||||
userId: locals.user ? locals.user.uuid : undefined,
|
||||
})
|
||||
|
||||
response.status(500).send({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.177.13",
|
||||
"version": "1.177.18",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -284,6 +284,7 @@ import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler
|
||||
import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
|
||||
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
|
||||
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
|
||||
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
constructor(private mode: 'server' | 'worker' = 'server') {}
|
||||
@@ -986,7 +987,18 @@ export class ContainerConfigLoader {
|
||||
.toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository)))
|
||||
container.bind<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser)
|
||||
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest)
|
||||
container.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken).to(RefreshSessionToken)
|
||||
container
|
||||
.bind<RefreshSessionToken>(TYPES.Auth_RefreshSessionToken)
|
||||
.toConstantValue(
|
||||
new RefreshSessionToken(
|
||||
container.get<SessionServiceInterface>(TYPES.Auth_SessionService),
|
||||
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
|
||||
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
|
||||
container.get<TimerInterface>(TYPES.Auth_Timer),
|
||||
container.get<GetSetting>(TYPES.Auth_GetSetting),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
container.bind<SignIn>(TYPES.Auth_SignIn).to(SignIn)
|
||||
container
|
||||
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
|
||||
@@ -1409,6 +1421,7 @@ export class ContainerConfigLoader {
|
||||
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
|
||||
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
|
||||
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
|
||||
container.get<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions),
|
||||
container.get<winston.Logger>(TYPES.Auth_Logger),
|
||||
),
|
||||
)
|
||||
@@ -1708,7 +1721,6 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
|
||||
.toConstantValue(
|
||||
new BaseUsersController(
|
||||
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
|
||||
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
|
||||
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
|
||||
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
|
||||
|
||||
@@ -34,6 +34,19 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
event.payload.timestamp,
|
||||
)
|
||||
|
||||
const renewalResult = await this.renewSharedSubscriptions.execute({
|
||||
inviterEmail: event.payload.userEmail,
|
||||
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 offline subscriptions: ${renewalResult.getError()}`, {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
}
|
||||
|
||||
await this.roleService.setOfflineUserRole(offlineUserSubscription)
|
||||
|
||||
return
|
||||
|
||||
@@ -16,6 +16,7 @@ import { OfflineSettingName } from '../Setting/OfflineSettingName'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
|
||||
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
|
||||
import { RenewSharedSubscriptions } from '../UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
|
||||
|
||||
export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@@ -27,11 +28,20 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
private setSettingValue: SetSettingValue,
|
||||
private offlineSettingService: OfflineSettingServiceInterface,
|
||||
private contentDecoder: ContentDecoderInterface,
|
||||
private renewSharedSubscriptions: RenewSharedSubscriptions,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
|
||||
this.logger.info('Subscription sync requested', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
if (event.payload.offline) {
|
||||
this.logger.info('Syncing offline subscription', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
const offlineUserSubscription = await this.createOrUpdateOfflineSubscription(
|
||||
event.payload.subscriptionId,
|
||||
event.payload.subscriptionName,
|
||||
@@ -41,6 +51,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
event.payload.timestamp,
|
||||
)
|
||||
|
||||
const renewalResult = await this.renewSharedSubscriptions.execute({
|
||||
inviterEmail: event.payload.userEmail,
|
||||
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 offline subscriptions for user: ${renewalResult.getError()}`, {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
}
|
||||
|
||||
await this.roleService.setOfflineUserRole(offlineUserSubscription)
|
||||
|
||||
const offlineFeaturesTokenDecoded = this.contentDecoder.decode(
|
||||
@@ -60,11 +83,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
value: offlineFeaturesTokenDecoded.extensionKey,
|
||||
})
|
||||
|
||||
this.logger.info('Offline subscription synced', {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const usernameOrError = Username.create(event.payload.userEmail)
|
||||
if (usernameOrError.isFailed()) {
|
||||
this.logger.warn(`Could not sync subscription: ${usernameOrError.getError()}`, {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
const username = usernameOrError.getValue()
|
||||
@@ -72,10 +103,18 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
const user = await this.userRepository.findOneByUsernameOrEmail(username)
|
||||
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with email: ${username.value}`)
|
||||
this.logger.warn(`Could not find user with email: ${username.value}`, {
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info('Syncing subscription', {
|
||||
userId: user.uuid,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
|
||||
const userSubscription = await this.createOrUpdateSubscription(
|
||||
event.payload.subscriptionId,
|
||||
event.payload.subscriptionName,
|
||||
@@ -85,6 +124,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
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: ${renewalResult.getError()}`, {
|
||||
userId: user.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
|
||||
|
||||
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
|
||||
@@ -107,6 +159,11 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(`Could not set extension key for user ${user.uuid}`)
|
||||
}
|
||||
|
||||
this.logger.info('Subscription synced', {
|
||||
userId: user.uuid,
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
})
|
||||
}
|
||||
|
||||
private async createOrUpdateSubscription(
|
||||
|
||||
@@ -7,6 +7,10 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { GetSetting } from './GetSetting/GetSetting'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { LogSessionUserAgentOption } from '@standardnotes/settings'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
|
||||
describe('RefreshSessionToken', () => {
|
||||
let sessionService: SessionServiceInterface
|
||||
@@ -14,16 +18,20 @@ describe('RefreshSessionToken', () => {
|
||||
let domainEventFactory: DomainEventFactoryInterface
|
||||
let domainEventPublisher: DomainEventPublisherInterface
|
||||
let timer: TimerInterface
|
||||
let getSetting: GetSetting
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () =>
|
||||
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, logger)
|
||||
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, getSetting, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
session.uuid = '1-2-3'
|
||||
session.refreshExpiration = new Date(123)
|
||||
|
||||
getSetting = {} as jest.Mocked<GetSetting>
|
||||
getSetting.execute = jest.fn().mockReturnValue(Result.fail('not found'))
|
||||
|
||||
sessionService = {} as jest.Mocked<SessionServiceInterface>
|
||||
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
|
||||
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
|
||||
@@ -69,6 +77,35 @@ describe('RefreshSessionToken', () => {
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should refresh session token and update user agent if enabled', async () => {
|
||||
getSetting.execute = jest.fn().mockReturnValue(
|
||||
Result.ok({
|
||||
setting: {} as jest.Mocked<Setting>,
|
||||
decryptedValue: LogSessionUserAgentOption.Enabled,
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
accessToken: '123',
|
||||
refreshToken: '234',
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
|
||||
})
|
||||
|
||||
expect(sessionService.refreshTokens).toHaveBeenCalledWith({ session, isEphemeral: false })
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
sessionPayload: {
|
||||
access_token: 'token1',
|
||||
refresh_token: 'token2',
|
||||
access_expiration: 123,
|
||||
refresh_expiration: 234,
|
||||
},
|
||||
})
|
||||
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should refresh a session token even if publishing domain event fails', async () => {
|
||||
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))
|
||||
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { SettingName } from '@standardnotes/domain-core'
|
||||
import { LogSessionUserAgentOption } from '@standardnotes/settings'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { SessionServiceInterface } from '../Session/SessionServiceInterface'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
|
||||
import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse'
|
||||
import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO'
|
||||
import { GetSetting } from './GetSetting/GetSetting'
|
||||
|
||||
@injectable()
|
||||
export class RefreshSessionToken {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface,
|
||||
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
|
||||
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
|
||||
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Auth_Logger) private logger: Logger,
|
||||
private sessionService: SessionServiceInterface,
|
||||
private domainEventFactory: DomainEventFactoryInterface,
|
||||
private domainEventPublisher: DomainEventPublisherInterface,
|
||||
private timer: TimerInterface,
|
||||
private getSetting: GetSetting,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> {
|
||||
@@ -46,7 +47,9 @@ export class RefreshSessionToken {
|
||||
}
|
||||
}
|
||||
|
||||
session.userAgent = dto.userAgent
|
||||
if (await this.isLoggingUserAgentEnabledOnSessions(session.userUuid)) {
|
||||
session.userAgent = dto.userAgent
|
||||
}
|
||||
|
||||
const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
|
||||
|
||||
@@ -64,4 +67,19 @@ export class RefreshSessionToken {
|
||||
userUuid: session.userUuid,
|
||||
}
|
||||
}
|
||||
|
||||
private async isLoggingUserAgentEnabledOnSessions(userUuid: string): Promise<boolean> {
|
||||
const loggingSettingOrError = await this.getSetting.execute({
|
||||
settingName: SettingName.NAMES.LogSessionUserAgent,
|
||||
decrypted: true,
|
||||
userUuid: userUuid,
|
||||
allowSensitiveRetrieval: true,
|
||||
})
|
||||
if (loggingSettingOrError.isFailed()) {
|
||||
return true
|
||||
}
|
||||
const loggingSetting = loggingSettingOrError.getValue()
|
||||
|
||||
return loggingSetting.decryptedValue === LogSessionUserAgentOption.Enabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ import {
|
||||
controller,
|
||||
httpDelete,
|
||||
httpGet,
|
||||
httpPatch,
|
||||
httpPut,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
|
||||
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
|
||||
import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
|
||||
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
|
||||
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
|
||||
@@ -21,26 +19,13 @@ import { BaseUsersController } from './Base/BaseUsersController'
|
||||
@controller('/users')
|
||||
export class AnnotatedUsersController extends BaseUsersController {
|
||||
constructor(
|
||||
@inject(TYPES.Auth_UpdateUser) override updateUser: UpdateUser,
|
||||
@inject(TYPES.Auth_DeleteAccount) override doDeleteAccount: DeleteAccount,
|
||||
@inject(TYPES.Auth_GetUserSubscription) override doGetUserSubscription: GetUserSubscription,
|
||||
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
|
||||
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
|
||||
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
|
||||
) {
|
||||
super(
|
||||
updateUser,
|
||||
doDeleteAccount,
|
||||
doGetUserSubscription,
|
||||
clearLoginAttempts,
|
||||
increaseLoginAttempts,
|
||||
changeCredentialsUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
|
||||
override async update(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
return super.update(request, response)
|
||||
super(doDeleteAccount, doGetUserSubscription, clearLoginAttempts, increaseLoginAttempts, changeCredentialsUseCase)
|
||||
}
|
||||
|
||||
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAtte
|
||||
import { SignIn } from '../../../Domain/UseCase/SignIn'
|
||||
import { VerifyMFA } from '../../../Domain/UseCase/VerifyMFA'
|
||||
import { AuthController } from '../../../Controller/AuthController'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
|
||||
export class BaseAuthController extends BaseHttpController {
|
||||
@@ -37,9 +38,11 @@ export class BaseAuthController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async params(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.session) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.session) {
|
||||
const result = await this.getUserKeyParams.execute({
|
||||
email: response.locals.user.email,
|
||||
email: locals.user.email,
|
||||
authenticated: true,
|
||||
})
|
||||
|
||||
@@ -145,6 +148,8 @@ export class BaseAuthController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (!request.body.code_challenge) {
|
||||
return this.json(
|
||||
{
|
||||
@@ -156,9 +161,9 @@ export class BaseAuthController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
if (response.locals.session) {
|
||||
if (locals.session) {
|
||||
const result = await this.getUserKeyParams.execute({
|
||||
email: response.locals.user.email,
|
||||
email: locals.user.email,
|
||||
authenticated: true,
|
||||
codeChallenge: request.body.code_challenge as string,
|
||||
})
|
||||
@@ -248,8 +253,10 @@ export class BaseAuthController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.authController.generateRecoveryCodes({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
@@ -280,8 +287,10 @@ export class BaseAuthController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.authController.signOut({
|
||||
readOnlyAccess: response.locals.readOnlyAccess,
|
||||
readOnlyAccess: locals.readOnlyAccess,
|
||||
authorizationHeader: <string>request.headers.authorization,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
|
||||
|
||||
import { AuthenticatorsController } from '../../../Controller/AuthenticatorsController'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseAuthenticatorsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -30,16 +31,20 @@ export class BaseAuthenticatorsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async list(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.authenticatorsController.list({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
async delete(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.authenticatorsController.delete({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
authenticatorId: request.params.authenticatorId,
|
||||
})
|
||||
|
||||
@@ -47,17 +52,21 @@ export class BaseAuthenticatorsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.authenticatorsController.generateRegistrationOptions({
|
||||
username: response.locals.user.email,
|
||||
userUuid: response.locals.user.uuid,
|
||||
username: locals.user.email,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.authenticatorsController.verifyRegistrationResponse({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
attestationResponse: request.body.attestationResponse,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
|
||||
|
||||
import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseFeaturesController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -17,7 +18,9 @@ export class BaseFeaturesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Request, Response } from 'express'
|
||||
|
||||
import { CreateListedAccount } from '../../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseListedController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -18,7 +19,9 @@ export class BaseListedController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async createListedAccount(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -31,8 +34,8 @@ export class BaseListedController extends BaseHttpController {
|
||||
}
|
||||
|
||||
await this.doCreateListedAccount.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userEmail: response.locals.user.email,
|
||||
userUuid: locals.user.uuid,
|
||||
userEmail: locals.user.email,
|
||||
})
|
||||
|
||||
return this.json({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AuthenticateOfflineSubscriptionToken } from '../../../Domain/UseCase/Au
|
||||
import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
|
||||
import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { GetUserOfflineSubscription } from '../../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
|
||||
import { OfflineResponseLocals } from '../OfflineResponseLocals'
|
||||
|
||||
export class BaseOfflineController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -30,8 +31,10 @@ export class BaseOfflineController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as OfflineResponseLocals
|
||||
|
||||
const result = await this.doGetUserFeatures.execute({
|
||||
email: response.locals.offlineUserEmail,
|
||||
email: locals.userEmail,
|
||||
offline: true,
|
||||
})
|
||||
|
||||
@@ -115,8 +118,10 @@ export class BaseOfflineController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as OfflineResponseLocals
|
||||
|
||||
const result = await this.getUserOfflineSubscription.execute({
|
||||
userEmail: response.locals.userEmail,
|
||||
userEmail: locals.userEmail,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ErrorTag } from '@standardnotes/responses'
|
||||
import { DeleteOtherSessionsForUser } from '../../../Domain/UseCase/DeleteOtherSessionsForUser'
|
||||
import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser'
|
||||
import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSessionController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -24,7 +25,9 @@ export class BaseSessionController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -36,7 +39,7 @@ export class BaseSessionController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
if (!request.body.uuid) {
|
||||
if (!request.body.uuid || !locals.session) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -47,7 +50,7 @@ export class BaseSessionController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
if (request.body.uuid === response.locals.session.uuid) {
|
||||
if (request.body.uuid === locals.session.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -59,7 +62,7 @@ export class BaseSessionController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const useCaseResponse = await this.deleteSessionForUser.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
sessionUuid: request.body.uuid,
|
||||
})
|
||||
|
||||
@@ -74,7 +77,7 @@ export class BaseSessionController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.statusCode(204)
|
||||
}
|
||||
@@ -83,7 +86,9 @@ export class BaseSessionController extends BaseHttpController {
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.JsonResult | results.StatusCodeResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -95,7 +100,7 @@ export class BaseSessionController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.locals.user) {
|
||||
if (!locals.user || !locals.session) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -107,12 +112,12 @@ export class BaseSessionController extends BaseHttpController {
|
||||
}
|
||||
|
||||
await this.deleteOtherSessionsForUser.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
currentSessionUuid: response.locals.session.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
currentSessionUuid: locals.session.uuid,
|
||||
markAsRevoked: true,
|
||||
})
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.statusCode(204)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Session } from '../../../Domain/Session/Session'
|
||||
import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { User } from '../../../Domain/User/User'
|
||||
import { SessionProjector } from '../../../Projection/SessionProjector'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSessionsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -67,12 +68,14 @@ export class BaseSessionsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json([])
|
||||
}
|
||||
|
||||
const useCaseResponse = await this.getActiveSessionsForUser.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(
|
||||
@@ -80,7 +83,7 @@ export class BaseSessionsController extends BaseHttpController {
|
||||
this.sessionProjector.projectCustom(
|
||||
SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
|
||||
session,
|
||||
response.locals.session,
|
||||
locals.session,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting
|
||||
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
|
||||
import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation'
|
||||
import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSettingsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -40,7 +41,9 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -86,7 +89,9 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -135,7 +140,9 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -147,7 +154,7 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -163,7 +170,7 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
const result = await this.setSettingValue.execute({
|
||||
settingName: name,
|
||||
value,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
checkUserPermissions: true,
|
||||
})
|
||||
|
||||
@@ -181,8 +188,8 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
|
||||
const triggerResult = await this.triggerPostSettingUpdateActions.execute({
|
||||
updatedSettingName: setting.props.name,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userEmail: response.locals.user.email,
|
||||
userUuid: locals.user.uuid,
|
||||
userEmail: locals.user.email,
|
||||
unencryptedValue: value,
|
||||
})
|
||||
if (triggerResult.isFailed()) {
|
||||
@@ -196,7 +203,9 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -208,7 +217,7 @@ export class BaseSettingsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { ApiVersion } from '@standardnotes/api'
|
||||
|
||||
import { SubscriptionInvitesController } from '../../../Controller/SubscriptionInvitesController'
|
||||
import { Role } from '../../../Domain/Role/Role'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
import { Role } from '@standardnotes/security'
|
||||
|
||||
export class BaseSubscriptionInvitesController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -23,12 +24,14 @@ export class BaseSubscriptionInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.subscriptionInvitesController.acceptInvite({
|
||||
api: request.query.api as ApiVersion,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
})
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
@@ -43,30 +46,36 @@ export class BaseSubscriptionInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.subscriptionInvitesController.invite({
|
||||
...request.body,
|
||||
inviterEmail: response.locals.user.email,
|
||||
inviterUuid: response.locals.user.uuid,
|
||||
inviterRoles: response.locals.roles.map((role: Role) => role.name),
|
||||
inviterEmail: locals.user.email,
|
||||
inviterUuid: locals.user.uuid,
|
||||
inviterRoles: locals.roles.map((role: Role) => role.name),
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.subscriptionInvitesController.cancelInvite({
|
||||
...request.body,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
inviterEmail: response.locals.user.email,
|
||||
inviterEmail: locals.user.email,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.subscriptionInvitesController.listInvites({
|
||||
...request.body,
|
||||
inviterEmail: response.locals.user.email,
|
||||
inviterEmail: locals.user.email,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GetSubscriptionSetting } from '../../../Domain/UseCase/GetSubscriptionS
|
||||
import { GetSharedOrRegularSubscriptionForUser } from '../../../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser'
|
||||
import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting'
|
||||
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSubscriptionSettingsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -22,8 +23,10 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
if (subscriptionOrError.isFailed()) {
|
||||
return this.json(
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscript
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
import { User } from '../../../Domain/User/User'
|
||||
import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSubscriptionTokensController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -29,7 +30,9 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async createToken(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -42,7 +45,7 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const result = await this.createSubscriptionToken.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BaseHttpController, results } from 'inversify-express-utils'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { UserRequestsController } from '../../../Controller/UserRequestsController'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseUserRequestsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -17,10 +18,12 @@ export class BaseUserRequestsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.userRequestsController.submitUserRequest({
|
||||
requestType: request.body.requestType,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userEmail: response.locals.user.email,
|
||||
userUuid: locals.user.uuid,
|
||||
userEmail: locals.user.email,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
|
||||
@@ -7,12 +7,11 @@ import { ClearLoginAttempts } from '../../../Domain/UseCase/ClearLoginAttempts'
|
||||
import { DeleteAccount } from '../../../Domain/UseCase/DeleteAccount/DeleteAccount'
|
||||
import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
|
||||
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
|
||||
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
|
||||
import { ErrorTag } from '@standardnotes/responses'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseUsersController extends BaseHttpController {
|
||||
constructor(
|
||||
protected updateUser: UpdateUser,
|
||||
protected doDeleteAccount: DeleteAccount,
|
||||
protected doGetUserSubscription: GetUserSubscription,
|
||||
protected clearLoginAttempts: ClearLoginAttempts,
|
||||
@@ -23,61 +22,16 @@ export class BaseUsersController extends BaseHttpController {
|
||||
super()
|
||||
|
||||
if (this.controllerContainer !== undefined) {
|
||||
this.controllerContainer.register('auth.users.update', this.update.bind(this))
|
||||
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
|
||||
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
|
||||
this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
async update(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
tag: ErrorTag.ReadOnlyAccess,
|
||||
message: 'Session has read-only access.',
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
if (request.params.userId !== response.locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: 'Operation not allowed.',
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
const updateResult = await this.updateUser.execute({
|
||||
user: response.locals.user,
|
||||
updatedWithUserAgent: <string>request.headers['user-agent'],
|
||||
apiVersion: request.body.api,
|
||||
})
|
||||
|
||||
if (updateResult.success) {
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
|
||||
return this.json(updateResult.authResponse)
|
||||
}
|
||||
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message: 'Could not update user.',
|
||||
},
|
||||
},
|
||||
400,
|
||||
)
|
||||
}
|
||||
|
||||
async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -107,7 +61,9 @@ export class BaseUsersController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (request.params.userUuid !== response.locals.user.uuid) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (request.params.userUuid !== locals.user.uuid) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -130,7 +86,9 @@ export class BaseUsersController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
if (response.locals.readOnlyAccess) {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
if (locals.readOnlyAccess) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -175,7 +133,7 @@ export class BaseUsersController extends BaseHttpController {
|
||||
400,
|
||||
)
|
||||
}
|
||||
const usernameOrError = Username.create(response.locals.user.email)
|
||||
const usernameOrError = Username.create(locals.user.email)
|
||||
if (usernameOrError.isFailed()) {
|
||||
return this.json(
|
||||
{
|
||||
@@ -202,7 +160,7 @@ export class BaseUsersController extends BaseHttpController {
|
||||
})
|
||||
|
||||
if (changeCredentialsResult.isFailed()) {
|
||||
await this.increaseLoginAttempts.execute({ email: response.locals.user.email })
|
||||
await this.increaseLoginAttempts.execute({ email: locals.user.email })
|
||||
|
||||
return this.json(
|
||||
{
|
||||
@@ -214,9 +172,9 @@ export class BaseUsersController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
await this.clearLoginAttempts.execute({ email: response.locals.user.email })
|
||||
await this.clearLoginAttempts.execute({ email: locals.user.email })
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.json(changeCredentialsResult.getValue())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseValetTokenController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -20,9 +21,11 @@ export class BaseValetTokenController extends BaseHttpController {
|
||||
}
|
||||
|
||||
public async create(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const payload: CreateValetTokenPayload = request.body
|
||||
|
||||
if (response.locals.readOnlyAccess && payload.operation !== 'read') {
|
||||
if (locals.readOnlyAccess && payload.operation !== 'read') {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
@@ -50,7 +53,7 @@ export class BaseValetTokenController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const createValetKeyResponse = await this.createValetKey.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
operation: payload.operation as ValetTokenOperation,
|
||||
resources: payload.resources,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/sec
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@@ -34,10 +35,12 @@ export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.user = token.user
|
||||
response.locals.roles = token.roles
|
||||
response.locals.session = token.session
|
||||
response.locals.readOnlyAccess = token.session?.readonly_access ?? false
|
||||
Object.assign(response.locals, {
|
||||
user: token.user,
|
||||
roles: token.roles,
|
||||
session: token.session,
|
||||
readOnlyAccess: token.session?.readonly_access ?? false,
|
||||
} as ResponseLocals)
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { OfflineResponseLocals } from '../OfflineResponseLocals'
|
||||
|
||||
@injectable()
|
||||
export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware {
|
||||
@@ -48,8 +49,10 @@ export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.featuresToken = token.featuresToken
|
||||
response.locals.userEmail = token.userEmail
|
||||
Object.assign(response.locals, {
|
||||
featuresToken: token.featuresToken,
|
||||
userEmail: token.userEmail,
|
||||
} as OfflineResponseLocals)
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
|
||||
@@ -44,8 +44,8 @@ describe('OfflineUserAuthMiddleware', () => {
|
||||
|
||||
await createMiddleware().handler(request, response, next)
|
||||
|
||||
expect(response.locals.offlineUserEmail).toEqual('test@test.com')
|
||||
expect(response.locals.offlineFeaturesToken).toEqual('offline-features-token')
|
||||
expect(response.locals.userEmail).toEqual('test@test.com')
|
||||
expect(response.locals.featuresToken).toEqual('offline-features-token')
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Logger } from 'winston'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { OfflineSettingName } from '../../../Domain/Setting/OfflineSettingName'
|
||||
import { OfflineSettingRepositoryInterface } from '../../../Domain/Setting/OfflineSettingRepositoryInterface'
|
||||
import { OfflineResponseLocals } from '../OfflineResponseLocals'
|
||||
|
||||
@injectable()
|
||||
export class OfflineUserAuthMiddleware extends BaseMiddleware {
|
||||
@@ -47,8 +48,10 @@ export class OfflineUserAuthMiddleware extends BaseMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
response.locals.offlineUserEmail = offlineFeaturesTokenSetting.email
|
||||
response.locals.offlineFeaturesToken = offlineFeaturesTokenSetting.value
|
||||
Object.assign(response.locals, {
|
||||
featuresToken: offlineFeaturesTokenSetting.value,
|
||||
userEmail: offlineFeaturesTokenSetting.email,
|
||||
} as OfflineResponseLocals)
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface OfflineResponseLocals {
|
||||
userEmail: string
|
||||
featuresToken: string
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Role } from '@standardnotes/security'
|
||||
|
||||
export interface ResponseLocals {
|
||||
user: {
|
||||
uuid: string
|
||||
email: string
|
||||
}
|
||||
roles: Array<Role>
|
||||
session?: {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
readOnlyAccess: boolean
|
||||
}
|
||||
@@ -84,6 +84,7 @@ export class TypeORMUserSubscriptionRepository implements UserSubscriptionReposi
|
||||
userUuid,
|
||||
subscriptionId,
|
||||
})
|
||||
.orderBy('ends_at', 'DESC')
|
||||
.getOne()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.37.9](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.8...@standardnotes/files-server@1.37.9) (2024-01-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
|
||||
|
||||
## [1.37.8](https://github.com/standardnotes/server/compare/@standardnotes/files-server@1.37.7...@standardnotes/files-server@1.37.8) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -24,6 +24,10 @@ void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
|
||||
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
|
||||
: '50mb'
|
||||
|
||||
const server = new InversifyExpressServer(container)
|
||||
|
||||
server.setConfig((app) => {
|
||||
@@ -58,9 +62,9 @@ void container.load().then((container) => {
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(raw({ limit: '50mb', type: 'application/octet-stream' }))
|
||||
app.use(urlencoded({ extended: true, limit: '50mb' }))
|
||||
app.use(json({ limit: requestPayloadLimit }))
|
||||
app.use(raw({ limit: requestPayloadLimit, type: 'application/octet-stream' }))
|
||||
app.use(urlencoded({ extended: true, limit: requestPayloadLimit }))
|
||||
app.use(
|
||||
cors({
|
||||
exposedHeaders: ['Content-Range', 'Accept-Ranges'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.37.8",
|
||||
"version": "1.37.9",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,84 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.58](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.57...@standardnotes/home-server@1.22.58) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.57](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.56...@standardnotes/home-server@1.22.57) (2024-01-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.56](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.55...@standardnotes/home-server@1.22.56) (2024-01-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.55](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.54...@standardnotes/home-server@1.22.55) (2024-01-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.54](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.53...@standardnotes/home-server@1.22.54) (2024-01-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.53](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.52...@standardnotes/home-server@1.22.53) (2024-01-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
|
||||
|
||||
## [1.22.52](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.51...@standardnotes/home-server@1.22.52) (2024-01-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.51](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.50...@standardnotes/home-server@1.22.51) (2024-01-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.50](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.49...@standardnotes/home-server@1.22.50) (2024-01-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.49](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.48...@standardnotes/home-server@1.22.49) (2024-01-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.48](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.47...@standardnotes/home-server@1.22.48) (2024-01-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.47](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.46...@standardnotes/home-server@1.22.47) (2024-01-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.46](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.45...@standardnotes/home-server@1.22.46) (2024-01-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.45](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.44...@standardnotes/home-server@1.22.45) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.44](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.43...@standardnotes/home-server@1.22.44) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.43](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.42...@standardnotes/home-server@1.22.43) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.42](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.41...@standardnotes/home-server@1.22.42) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.41](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.40...@standardnotes/home-server@1.22.41) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.40](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.39...@standardnotes/home-server@1.22.40) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
## [1.22.39](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.38...@standardnotes/home-server@1.22.39) (2024-01-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/home-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/home-server",
|
||||
"version": "1.22.39",
|
||||
"version": "1.22.58",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -53,6 +53,10 @@ export class HomeServer implements HomeServerInterface {
|
||||
const env: Env = new Env(environmentOverrides)
|
||||
env.load()
|
||||
|
||||
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
|
||||
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
|
||||
: '50mb'
|
||||
|
||||
this.configureLoggers(env, configuration)
|
||||
|
||||
const apiGatewayService = new ApiGatewayService(serviceContainer)
|
||||
@@ -114,8 +118,8 @@ export class HomeServer implements HomeServerInterface {
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(raw({ limit: '50mb', type: 'application/octet-stream' }))
|
||||
app.use(json({ limit: requestPayloadLimit }))
|
||||
app.use(raw({ limit: requestPayloadLimit, type: 'application/octet-stream' }))
|
||||
app.use(
|
||||
text({
|
||||
type: [
|
||||
@@ -160,8 +164,24 @@ export class HomeServer implements HomeServerInterface {
|
||||
const logger: winston.Logger = winston.loggers.get('home-server')
|
||||
|
||||
server.setErrorConfig((app) => {
|
||||
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
|
||||
logger.error(error.stack)
|
||||
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
|
||||
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,
|
||||
})
|
||||
|
||||
if ('type' in error && error.type === 'entity.too.large') {
|
||||
response.status(413).send({
|
||||
error: {
|
||||
message: 'The request payload is too large.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.status(500).send({
|
||||
error: {
|
||||
|
||||
@@ -3,6 +3,86 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.134.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.134.0...@standardnotes/syncing-server@1.134.1) (2024-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** missing item operations metric store expiry ([89dc6c1](https://github.com/standardnotes/server/commit/89dc6c19bf0e4a8c715f085ace5a717151d8fe9f))
|
||||
|
||||
# [1.134.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.6...@standardnotes/syncing-server@1.134.0) (2024-01-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** reduced abuse thresholds for free users ([#1021](https://github.com/standardnotes/server/issues/1021)) ([0443de8](https://github.com/standardnotes/server/commit/0443de88ceae3cb7c0793a3457753806b51db6e2))
|
||||
|
||||
## [1.133.6](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.5...@standardnotes/syncing-server@1.133.6) (2024-01-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add dedicated http code response upon a request with too large payload ([#1019](https://github.com/standardnotes/server/issues/1019)) ([6062f85](https://github.com/standardnotes/server/commit/6062f850000477983315d2d9b7c913956f755ebb))
|
||||
|
||||
## [1.133.5](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.4...@standardnotes/syncing-server@1.133.5) (2024-01-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.133.4](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.3...@standardnotes/syncing-server@1.133.4) (2024-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** disable ot tracing ([a881dd2](https://github.com/standardnotes/server/commit/a881dd2d79d0e394b0b4f5d07e63821c93d45219))
|
||||
|
||||
## [1.133.3](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.2...@standardnotes/syncing-server@1.133.3) (2024-01-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** add traffic abuse check in gRPC coms ([e3cb1fa](https://github.com/standardnotes/server/commit/e3cb1faba46bbfd8741f6c827daa9438934dd710))
|
||||
* **syncing-server:** remove excessive debug logs ([5c5f988](https://github.com/standardnotes/server/commit/5c5f9880556f14f5cbe4599ac0d639c970495056))
|
||||
|
||||
## [1.133.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.1...@standardnotes/syncing-server@1.133.2) (2024-01-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** add debug logs to redis metrics store ([a4ad37f](https://github.com/standardnotes/server/commit/a4ad37f30948ba4292f367240c3dbfca916282ac))
|
||||
* **syncing-server:** add metadata to transfer breach logs ([73c2cc1](https://github.com/standardnotes/server/commit/73c2cc1222b647e82de3755c7e28d283cd9f872f))
|
||||
|
||||
## [1.133.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.0...@standardnotes/syncing-server@1.133.1) (2024-01-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** add debug logs for checking traffic abuse ([1f4b26d](https://github.com/standardnotes/server/commit/1f4b26d269a92f5b43455ce3a3cf3d4f15f0d099))
|
||||
* **syncing-server:** error message ([e253825](https://github.com/standardnotes/server/commit/e253825da69d1be6d7bb4d0360f8c3add73516ef))
|
||||
* **syncing-server:** metadata in logs ([02f4d5c](https://github.com/standardnotes/server/commit/02f4d5c717cef1014930f11b2792868967812042))
|
||||
|
||||
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.132.0...@standardnotes/syncing-server@1.133.0) (2024-01-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** add traffic abuse checks ([#1014](https://github.com/standardnotes/server/issues/1014)) ([b717334](https://github.com/standardnotes/server/commit/b7173346d2949269b762b023da9ea67b7f327c35))
|
||||
|
||||
# [1.132.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.131.0...@standardnotes/syncing-server@1.132.0) (2024-01-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** send user based metrics to cloudwatch ([0c3bc0c](https://github.com/standardnotes/server/commit/0c3bc0cae654a6783f85e86995f978cc458d8b5c))
|
||||
|
||||
# [1.131.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.3...@standardnotes/syncing-server@1.131.0) (2024-01-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **syncing-server:** store per user content size utilization and item operations metrics ([4dd2eb9](https://github.com/standardnotes/server/commit/4dd2eb9349eb16006d1ebba99c848b8f6c51baf9))
|
||||
|
||||
## [1.130.3](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.2...@standardnotes/syncing-server@1.130.3) (2024-01-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** decrease metric expiration time ([f1aa431](https://github.com/standardnotes/server/commit/f1aa431c223714e56942a32ab75e380baf4f84aa))
|
||||
|
||||
## [1.130.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.1...@standardnotes/syncing-server@1.130.2) (2024-01-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** amount of minutes to process for metrics ([92bb447](https://github.com/standardnotes/server/commit/92bb447cacd0a40f963c2bfa5e61cb0f451959e6))
|
||||
|
||||
## [1.130.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.130.0...@standardnotes/syncing-server@1.130.1) (2024-01-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { OpenTelemetrySDK } from '@standardnotes/domain-events-infra'
|
||||
import { MapperInterface, ServiceIdentifier } from '@standardnotes/domain-core'
|
||||
|
||||
const sdk = new OpenTelemetrySDK({ serviceName: ServiceIdentifier.NAMES.SyncingServer })
|
||||
sdk.start()
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/AnnotatedFallbackController'
|
||||
import '../src/Infra/InversifyExpressUtils/AnnotatedHealthCheckController'
|
||||
@@ -29,12 +25,17 @@ import { SyncingServer } from '../src/Infra/gRPC/SyncingServer'
|
||||
import { SyncItems } from '../src/Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { SyncResponseFactoryResolverInterface } from '../src/Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
|
||||
import { SyncResponse20200115 } from '../src/Domain/Item/SyncResponse/SyncResponse20200115'
|
||||
import { CheckForTrafficAbuse } from '../src/Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const requestPayloadLimit = env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)
|
||||
? `${+env.get('HTTP_REQUEST_PAYLOAD_LIMIT_MEGABYTES', true)}mb`
|
||||
: '50mb'
|
||||
|
||||
const server = new InversifyExpressServer(container)
|
||||
|
||||
server.setConfig((app) => {
|
||||
@@ -64,8 +65,8 @@ void container.load().then((container) => {
|
||||
}
|
||||
}))
|
||||
/* eslint-enable */
|
||||
app.use(json({ limit: '50mb' }))
|
||||
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 5000 }))
|
||||
app.use(json({ limit: requestPayloadLimit }))
|
||||
app.use(urlencoded({ extended: true, limit: requestPayloadLimit, parameterLimit: 5000 }))
|
||||
app.use(cors())
|
||||
})
|
||||
|
||||
@@ -114,6 +115,14 @@ void container.load().then((container) => {
|
||||
container.get<SyncItems>(TYPES.Sync_SyncItems),
|
||||
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
|
||||
container.get<MapperInterface<SyncResponse20200115, SyncResponse>>(TYPES.Sync_SyncResponseGRPCMapper),
|
||||
container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
|
||||
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
|
||||
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
|
||||
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
|
||||
container.get<winston.Logger>(TYPES.Sync_Logger),
|
||||
)
|
||||
|
||||
@@ -146,14 +155,6 @@ void container.load().then((container) => {
|
||||
logger.info('gRPC server closed')
|
||||
}
|
||||
})
|
||||
sdk
|
||||
.shutdown()
|
||||
.then(() => {
|
||||
logger.info('OpenTelemetry SDK shut down')
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Failed to shut down OpenTelemetry SDK: ${error.message}`)
|
||||
})
|
||||
})
|
||||
|
||||
logger.info(`Server started on port ${process.env.PORT}`)
|
||||
|
||||
@@ -19,7 +19,7 @@ const sendStatistics = async (
|
||||
region: awsRegion,
|
||||
})
|
||||
|
||||
const minutesToProcess = 60
|
||||
const minutesToProcess = 30
|
||||
|
||||
const metricsToProcess = [Metric.NAMES.ItemCreated, Metric.NAMES.ItemUpdated]
|
||||
|
||||
@@ -28,7 +28,7 @@ const sendStatistics = async (
|
||||
const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
|
||||
const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
|
||||
|
||||
const statistics = await metricsStore.getStatistics(
|
||||
const statistics = await metricsStore.getMetricsSummary(
|
||||
metricToProcess,
|
||||
timestamp,
|
||||
timestamp + Time.MicrosecondsInAMinute,
|
||||
@@ -57,6 +57,38 @@ const sendStatistics = async (
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const userMetricsToProcess = [Metric.NAMES.ItemOperation, Metric.NAMES.ContentSizeUtilized]
|
||||
for (const metricToProcess of userMetricsToProcess) {
|
||||
for (let i = 0; i <= minutesToProcess; i++) {
|
||||
const dateNMinutesAgo = timer.getUTCDateNMinutesAgo(minutesToProcess - i)
|
||||
const timestamp = timer.convertDateToMicroseconds(dateNMinutesAgo)
|
||||
|
||||
const statistics = await metricsStore.getUserBasedMetricsSummary(metricToProcess, timestamp)
|
||||
|
||||
if (statistics.sampleCount === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
await cloudwatchClient.send(
|
||||
new PutMetricDataCommand({
|
||||
Namespace: 'SyncingServer',
|
||||
MetricData: [
|
||||
{
|
||||
MetricName: metricToProcess,
|
||||
Timestamp: dateNMinutesAgo,
|
||||
StatisticValues: {
|
||||
Maximum: statistics.max,
|
||||
Minimum: statistics.min,
|
||||
SampleCount: statistics.sampleCount,
|
||||
Sum: statistics.sum,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const container = new ContainerConfigLoader('worker')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.130.1",
|
||||
"version": "1.134.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <21.0.0"
|
||||
},
|
||||
|
||||
@@ -166,6 +166,7 @@ import { SendEventToClients } from '../Domain/UseCase/Syncing/SendEventToClients
|
||||
import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
|
||||
import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
|
||||
import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
|
||||
import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
@@ -472,6 +473,49 @@ export class ContainerConfigLoader {
|
||||
})
|
||||
|
||||
// env vars
|
||||
container
|
||||
.bind(TYPES.Sync_STRICT_ABUSE_PROTECTION)
|
||||
.toConstantValue(env.get('STRICT_ABUSE_PROTECTION', true) === 'true')
|
||||
container
|
||||
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD)
|
||||
.toConstantValue(
|
||||
env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) ? +env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) : 1000,
|
||||
)
|
||||
container
|
||||
.bind(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD)
|
||||
.toConstantValue(
|
||||
env.get('FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD', true)
|
||||
? +env.get('FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD', true)
|
||||
: 500,
|
||||
)
|
||||
container
|
||||
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
|
||||
.toConstantValue(
|
||||
env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
|
||||
? +env.get('ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
|
||||
: 5,
|
||||
)
|
||||
container
|
||||
.bind(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD)
|
||||
.toConstantValue(
|
||||
env.get('UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
|
||||
? +env.get('UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
|
||||
: 100_000_000,
|
||||
)
|
||||
container
|
||||
.bind(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD)
|
||||
.toConstantValue(
|
||||
env.get('FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
|
||||
? +env.get('FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
|
||||
: 50_000_000,
|
||||
)
|
||||
container
|
||||
.bind(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
|
||||
.toConstantValue(
|
||||
env.get('UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
|
||||
? +env.get('UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true)
|
||||
: 5,
|
||||
)
|
||||
container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||
container
|
||||
.bind(TYPES.Sync_REVISIONS_FREQUENCY)
|
||||
@@ -557,7 +601,11 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<MetricsStoreInterface>(TYPES.Sync_MetricsStore)
|
||||
.toConstantValue(
|
||||
new RedisMetricStore(container.get<Redis>(TYPES.Sync_Redis), container.get<TimerInterface>(TYPES.Sync_Timer)),
|
||||
new RedisMetricStore(
|
||||
container.get<Redis>(TYPES.Sync_Redis),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -933,6 +981,15 @@ export class ContainerConfigLoader {
|
||||
context.container.get(TYPES.Sync_SyncResponseFactory20200115),
|
||||
)
|
||||
})
|
||||
container
|
||||
.bind<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse)
|
||||
.toConstantValue(
|
||||
new CheckForTrafficAbuse(
|
||||
container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
|
||||
container.get<TimerInterface>(TYPES.Sync_Timer),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
),
|
||||
)
|
||||
|
||||
// Handlers
|
||||
container
|
||||
@@ -1094,11 +1151,20 @@ export class ContainerConfigLoader {
|
||||
.bind<BaseItemsController>(TYPES.Sync_BaseItemsController)
|
||||
.toConstantValue(
|
||||
new BaseItemsController(
|
||||
container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
|
||||
container.get<SyncItems>(TYPES.Sync_SyncItems),
|
||||
container.get<CheckIntegrity>(TYPES.Sync_CheckIntegrity),
|
||||
container.get<GetItem>(TYPES.Sync_GetItem),
|
||||
container.get<MapperInterface<Item, ItemHttpRepresentation>>(TYPES.Sync_ItemHttpMapper),
|
||||
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
|
||||
container.get<Logger>(TYPES.Sync_Logger),
|
||||
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
|
||||
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
|
||||
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD),
|
||||
container.get<number>(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
|
||||
container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -42,6 +42,17 @@ const TYPES = {
|
||||
Sync_VALET_TOKEN_SECRET: Symbol.for('Sync_VALET_TOKEN_SECRET'),
|
||||
Sync_VALET_TOKEN_TTL: Symbol.for('Sync_VALET_TOKEN_TTL'),
|
||||
Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING: Symbol.for('Sync_IS_CONFIGURED_FOR_HOME_SERVER_OR_SELF_HOSTING'),
|
||||
Sync_STRICT_ABUSE_PROTECTION: Symbol.for('Sync_STRICT_ABUSE_PROTECTION'),
|
||||
Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
|
||||
'Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
|
||||
),
|
||||
Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
|
||||
Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
|
||||
Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD: Symbol.for('Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD'),
|
||||
Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD: Symbol.for('Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD'),
|
||||
Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for(
|
||||
'Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
|
||||
),
|
||||
// use cases
|
||||
Sync_SyncItems: Symbol.for('Sync_SyncItems'),
|
||||
Sync_CheckIntegrity: Symbol.for('Sync_CheckIntegrity'),
|
||||
@@ -85,6 +96,7 @@ const TYPES = {
|
||||
Sync_TransferSharedVault: Symbol.for('Sync_TransferSharedVault'),
|
||||
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
|
||||
Sync_DumpItem: Symbol.for('Sync_DumpItem'),
|
||||
Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
|
||||
// Handlers
|
||||
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
|
||||
Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
DomainEventPublisherInterface,
|
||||
EmailBackupRequestedEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { EmailLevel } from '@standardnotes/domain-core'
|
||||
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
|
||||
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
|
||||
@@ -32,6 +32,17 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
event: EmailBackupRequestedEvent,
|
||||
itemRepository: ItemRepositoryInterface,
|
||||
): Promise<void> {
|
||||
const userUuidOrError = Uuid.create(event.payload.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
this.logger.error('User uuid is invalid', {
|
||||
userId: event.payload.userUuid,
|
||||
codeTag: 'EmailBackupRequestedEventHandler',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const itemQuery: ItemQuery = {
|
||||
userUuid: event.payload.userUuid,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
@@ -42,6 +53,7 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
|
||||
const itemUuidBundles = await this.itemTransferCalculator.computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors,
|
||||
this.emailAttachmentMaxByteSize,
|
||||
userUuid,
|
||||
)
|
||||
|
||||
const backupFileNames: string[] = []
|
||||
|
||||
@@ -4,13 +4,18 @@ import { Logger } from 'winston'
|
||||
|
||||
import { ItemTransferCalculator } from './ItemTransferCalculator'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
describe('ItemTransferCalculator', () => {
|
||||
let logger: Logger
|
||||
|
||||
const createCalculator = () => new ItemTransferCalculator(logger)
|
||||
|
||||
let userUuid: Uuid
|
||||
|
||||
beforeEach(() => {
|
||||
userUuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
@@ -23,7 +28,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50, userUuid)
|
||||
|
||||
expect(result).toEqual({
|
||||
uuids: [
|
||||
@@ -42,7 +47,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40, userUuid)
|
||||
|
||||
expect(result).toEqual({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
@@ -57,7 +62,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50, userUuid)
|
||||
|
||||
expect(result).toEqual({
|
||||
uuids: [
|
||||
@@ -76,7 +81,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
|
||||
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40, userUuid)
|
||||
|
||||
expect(result).toEqual({
|
||||
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
@@ -93,7 +98,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50, userUuid)
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
@@ -111,7 +116,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40, userUuid)
|
||||
|
||||
expect(result).toEqual([
|
||||
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
@@ -126,7 +131,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50, userUuid)
|
||||
|
||||
expect(result).toEqual([
|
||||
[
|
||||
@@ -144,7 +149,7 @@ describe('ItemTransferCalculator', () => {
|
||||
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
|
||||
]
|
||||
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
|
||||
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40, userUuid)
|
||||
|
||||
expect(result).toEqual([
|
||||
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Logger } from 'winston'
|
||||
|
||||
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
constructor(private logger: Logger) {}
|
||||
@@ -9,6 +10,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
async computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
userUuid: Uuid,
|
||||
): Promise<{ uuids: Array<string>; transferLimitBreachedBeforeEndOfItems: boolean }> {
|
||||
const itemUuidsToFetch = []
|
||||
let totalContentSizeInBytes = 0
|
||||
@@ -24,6 +26,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
bytesTransferLimit,
|
||||
itemUuidsToFetch,
|
||||
itemContentSizeDescriptors,
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (transferLimitBreached) {
|
||||
@@ -41,6 +44,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
async computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
userUuid: Uuid,
|
||||
): Promise<Array<Array<string>>> {
|
||||
let itemUuidsToFetch = []
|
||||
let totalContentSizeInBytes = 0
|
||||
@@ -56,6 +60,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
bytesTransferLimit,
|
||||
itemUuidsToFetch,
|
||||
itemContentSizeDescriptors,
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (transferLimitBreached) {
|
||||
@@ -77,15 +82,20 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
|
||||
bytesTransferLimit: number
|
||||
itemUuidsToFetch: Array<string>
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[]
|
||||
userUuid: Uuid
|
||||
}): boolean {
|
||||
const transferLimitBreached = dto.totalContentSizeInBytes >= dto.bytesTransferLimit
|
||||
const transferLimitBreachedAtFirstItem =
|
||||
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizeDescriptors.length > 1
|
||||
|
||||
if (transferLimitBreachedAtFirstItem) {
|
||||
this.logger.warn(
|
||||
`Item ${dto.itemUuidsToFetch[0]} is breaching the content size transfer limit: ${dto.bytesTransferLimit}`,
|
||||
)
|
||||
this.logger.warn('Item is breaching the content size transfer limit at first item in the bundle to fetch.', {
|
||||
codeTag: 'ItemTransferCalculator',
|
||||
itemUuid: dto.itemUuidsToFetch[0],
|
||||
totalContentSizeInBytes: dto.totalContentSizeInBytes,
|
||||
bytesTransferLimit: dto.bytesTransferLimit,
|
||||
userId: dto.userUuid.value,
|
||||
})
|
||||
}
|
||||
|
||||
return transferLimitBreached && !transferLimitBreachedAtFirstItem
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
|
||||
|
||||
export interface ItemTransferCalculatorInterface {
|
||||
computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
userUuid: Uuid,
|
||||
): Promise<{ uuids: Array<string>; transferLimitBreachedBeforeEndOfItems: boolean }>
|
||||
computeItemUuidBundlesToFetch(
|
||||
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
|
||||
bytesTransferLimit: number,
|
||||
userUuid: Uuid,
|
||||
): Promise<Array<Array<string>>>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export class Metric extends ValueObject<MetricProps> {
|
||||
static readonly NAMES = {
|
||||
ItemCreated: 'ItemCreated',
|
||||
ItemUpdated: 'ItemUpdated',
|
||||
ContentSizeUtilized: 'ContentSizeUtilized',
|
||||
ItemOperation: 'ItemOperation',
|
||||
}
|
||||
|
||||
static create(props: MetricProps): Result<Metric> {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { Metric } from './Metric'
|
||||
import { MetricsSummary } from './MetricsSummary'
|
||||
|
||||
export interface MetricsStoreInterface {
|
||||
storeMetric(metric: Metric): Promise<void>
|
||||
getStatistics(
|
||||
name: string,
|
||||
from: number,
|
||||
to: number,
|
||||
): Promise<{
|
||||
sum: number
|
||||
max: number
|
||||
min: number
|
||||
sampleCount: number
|
||||
}>
|
||||
storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void>
|
||||
getUserBasedMetricsSummaryWithinTimeRange(dto: {
|
||||
metricName: string
|
||||
userUuid: Uuid
|
||||
from: Date
|
||||
to: Date
|
||||
}): Promise<MetricsSummary>
|
||||
getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary>
|
||||
getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface MetricsSummary {
|
||||
sum: number
|
||||
max: number
|
||||
min: number
|
||||
sampleCount: number
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
|
||||
import { CheckForTrafficAbuse } from './CheckForTrafficAbuse'
|
||||
import { MetricsSummary } from '../../../Metrics/MetricsSummary'
|
||||
import { Metric } from '../../../Metrics/Metric'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('CheckForTrafficAbuse', () => {
|
||||
let metricsStore: MetricsStoreInterface
|
||||
let timer: TimerInterface
|
||||
let timeframeLengthInMinutes: number
|
||||
let threshold: number
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new CheckForTrafficAbuse(metricsStore, timer, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
|
||||
const metricsSummary: MetricsSummary = {
|
||||
sum: 101,
|
||||
max: 0,
|
||||
min: 0,
|
||||
sampleCount: 0,
|
||||
}
|
||||
|
||||
metricsStore = {} as jest.Mocked<MetricsStoreInterface>
|
||||
metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
|
||||
|
||||
timer = {} as TimerInterface
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(0)
|
||||
timer.getUTCDateNMinutesAgo = jest.fn().mockReturnValue(new Date(0))
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(10))
|
||||
|
||||
timeframeLengthInMinutes = 5
|
||||
|
||||
threshold = 100
|
||||
})
|
||||
|
||||
it('returns a failure result if the user uuid is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: 'invalid',
|
||||
metricToCheck: Metric.NAMES.ItemOperation,
|
||||
timeframeLengthInMinutes,
|
||||
threshold,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('return a failure result if the metric name is invalid', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
metricToCheck: 'invalid',
|
||||
timeframeLengthInMinutes,
|
||||
threshold,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a failure result if the metric summary is above the threshold', async () => {
|
||||
const metricsSummary: MetricsSummary = {
|
||||
sum: 101,
|
||||
max: 0,
|
||||
min: 0,
|
||||
sampleCount: 0,
|
||||
}
|
||||
|
||||
metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
metricToCheck: Metric.NAMES.ItemOperation,
|
||||
timeframeLengthInMinutes,
|
||||
threshold,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns a success result if the metric summary is below the threshold', async () => {
|
||||
const metricsSummary: MetricsSummary = {
|
||||
sum: 99,
|
||||
max: 0,
|
||||
min: 0,
|
||||
sampleCount: 0,
|
||||
}
|
||||
|
||||
metricsStore.getUserBasedMetricsSummaryWithinTimeRange = jest.fn().mockReturnValue(metricsSummary)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userUuid: '00000000-0000-0000-0000-000000000000',
|
||||
metricToCheck: Metric.NAMES.ItemOperation,
|
||||
timeframeLengthInMinutes,
|
||||
threshold,
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { CheckForTrafficAbuseDTO } from './CheckForTrafficAbuseDTO'
|
||||
import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
|
||||
import { Metric } from '../../../Metrics/Metric'
|
||||
import { MetricsSummary } from '../../../Metrics/MetricsSummary'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class CheckForTrafficAbuse implements UseCaseInterface<MetricsSummary> {
|
||||
constructor(
|
||||
private metricsStore: MetricsStoreInterface,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: CheckForTrafficAbuseDTO): Promise<Result<MetricsSummary>> {
|
||||
this.logger.debug(`Checking for traffic abuse for metric: ${dto.metricToCheck}.`, {
|
||||
codeTag: 'CheckForTrafficAbuse',
|
||||
userId: dto.userUuid,
|
||||
})
|
||||
|
||||
const userUuidOrError = Uuid.create(dto.userUuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return Result.fail(userUuidOrError.getError())
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const metricToCheckOrError = Metric.create({
|
||||
name: dto.metricToCheck,
|
||||
timestamp: this.timer.getTimestampInMicroseconds(),
|
||||
})
|
||||
if (metricToCheckOrError.isFailed()) {
|
||||
return Result.fail(metricToCheckOrError.getError())
|
||||
}
|
||||
const metricToCheck = metricToCheckOrError.getValue()
|
||||
|
||||
const metricsSummary = await this.metricsStore.getUserBasedMetricsSummaryWithinTimeRange({
|
||||
metricName: metricToCheck.props.name,
|
||||
userUuid,
|
||||
from: this.timer.getUTCDateNMinutesAgo(dto.timeframeLengthInMinutes),
|
||||
to: this.timer.getUTCDate(),
|
||||
})
|
||||
|
||||
this.logger.debug(
|
||||
`Current traffic abuse metric ${dto.metricToCheck} value in timeframe of ${dto.timeframeLengthInMinutes} minutes is ${metricsSummary.sum}. The threshold is ${dto.threshold}`,
|
||||
{
|
||||
codeTag: 'CheckForTrafficAbuse',
|
||||
userId: dto.userUuid,
|
||||
},
|
||||
)
|
||||
|
||||
if (metricsSummary.sum > dto.threshold) {
|
||||
return Result.fail(
|
||||
`Traffic abuse detected for metric: ${metricToCheck.props.name}. Usage ${metricsSummary.sum} is greater than threshold ${dto.threshold}`,
|
||||
)
|
||||
}
|
||||
|
||||
return Result.ok(metricsSummary)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface CheckForTrafficAbuseDTO {
|
||||
userUuid: string
|
||||
metricToCheck: string
|
||||
timeframeLengthInMinutes: number
|
||||
threshold: number
|
||||
}
|
||||
@@ -63,6 +63,7 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
|
||||
const { uuids, transferLimitBreachedBeforeEndOfItems } = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||
itemContentSizeDescriptors,
|
||||
this.contentSizeTransferLimit,
|
||||
userUuid,
|
||||
)
|
||||
let items: Array<Item> = []
|
||||
if (uuids.length > 0) {
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('SaveNewItem', () => {
|
||||
|
||||
metricsStore = {} as jest.Mocked<MetricsStoreInterface>
|
||||
metricsStore.storeMetric = jest.fn()
|
||||
metricsStore.storeUserBasedMetric = jest.fn()
|
||||
|
||||
item1 = Item.create(
|
||||
{
|
||||
|
||||
@@ -110,6 +110,7 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
return Result.fail(itemOrError.getError())
|
||||
}
|
||||
const newItem = itemOrError.getValue()
|
||||
newItem.props.contentSize = Buffer.byteLength(JSON.stringify(newItem))
|
||||
|
||||
if (dto.itemHash.representsASharedVaultItem()) {
|
||||
const sharedVaultAssociationOrError = SharedVaultAssociation.create({
|
||||
@@ -138,6 +139,15 @@ export class SaveNewItem implements UseCaseInterface<Item> {
|
||||
|
||||
await this.itemRepository.insert(newItem)
|
||||
|
||||
await this.metricsStore.storeUserBasedMetric(
|
||||
Metric.create({
|
||||
name: Metric.NAMES.ContentSizeUtilized,
|
||||
timestamp: this.timer.getTimestampInMicroseconds(),
|
||||
}).getValue(),
|
||||
newItem.props.contentSize,
|
||||
userUuid,
|
||||
)
|
||||
|
||||
await this.metricsStore.storeMetric(
|
||||
Metric.create({ name: Metric.NAMES.ItemCreated, timestamp: this.timer.getTimestampInMicroseconds() }).getValue(),
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('UpdateExistingItem', () => {
|
||||
|
||||
metricsStore = {} as jest.Mocked<MetricsStoreInterface>
|
||||
metricsStore.storeMetric = jest.fn()
|
||||
metricsStore.storeUserBasedMetric = jest.fn()
|
||||
|
||||
item1 = Item.create(
|
||||
{
|
||||
|
||||
@@ -180,6 +180,15 @@ export class UpdateExistingItem implements UseCaseInterface<Item> {
|
||||
Metric.create({ name: Metric.NAMES.ItemUpdated, timestamp: this.timer.getTimestampInMicroseconds() }).getValue(),
|
||||
)
|
||||
|
||||
await this.metricsStore.storeUserBasedMetric(
|
||||
Metric.create({
|
||||
name: Metric.NAMES.ContentSizeUtilized,
|
||||
timestamp: this.timer.getTimestampInMicroseconds(),
|
||||
}).getValue(),
|
||||
dto.existingItem.props.contentSize,
|
||||
userUuid,
|
||||
)
|
||||
|
||||
/* istanbul ignore next */
|
||||
const revisionsFrequency = dto.isFreeUser ? this.freeRevisionFrequency : this.premiumRevisionFrequency
|
||||
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
|
||||
import { Metric } from '../../Domain/Metrics/Metric'
|
||||
import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
|
||||
|
||||
export class DummyMetricStore implements MetricsStoreInterface {
|
||||
async getUserBasedMetricsSummaryWithinTimeRange(_dto: {
|
||||
metricName: string
|
||||
userUuid: Uuid
|
||||
from: Date
|
||||
to: Date
|
||||
}): Promise<MetricsSummary> {
|
||||
return {
|
||||
sum: 0,
|
||||
max: 0,
|
||||
min: 0,
|
||||
sampleCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async getUserBasedMetricsSummary(_name: string, _timestamp: number): Promise<MetricsSummary> {
|
||||
return {
|
||||
sum: 0,
|
||||
max: 0,
|
||||
min: 0,
|
||||
sampleCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async storeUserBasedMetric(_metric: Metric, _value: number, _userUuid: Uuid): Promise<void> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
async storeMetric(_metric: Metric): Promise<void> {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
async getStatistics(
|
||||
_name: string,
|
||||
_from: number,
|
||||
_to: number,
|
||||
): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
|
||||
async getMetricsSummary(_name: string, _from: number, _to: number): Promise<MetricsSummary> {
|
||||
return {
|
||||
sum: 0,
|
||||
max: 0,
|
||||
|
||||
@@ -11,18 +11,47 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { BaseItemsController } from './Base/BaseItemsController'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { ItemHttpRepresentation } from '../../Mapping/Http/ItemHttpRepresentation'
|
||||
import { CheckForTrafficAbuse } from '../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@controller('/items', TYPES.Sync_AuthMiddleware)
|
||||
export class AnnotatedItemsController extends BaseItemsController {
|
||||
constructor(
|
||||
@inject(TYPES.Sync_CheckForTrafficAbuse) override checkForTrafficAbuse: CheckForTrafficAbuse,
|
||||
@inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
|
||||
@inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
|
||||
@inject(TYPES.Sync_GetItem) override getItem: GetItem,
|
||||
@inject(TYPES.Sync_ItemHttpMapper) override itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
@inject(TYPES.Sync_SyncResponseFactoryResolver)
|
||||
override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
|
||||
@inject(TYPES.Sync_Logger) override logger: Logger,
|
||||
@inject(TYPES.Sync_STRICT_ABUSE_PROTECTION) override strictAbuseProtection: boolean,
|
||||
@inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
|
||||
override itemOperationsAbuseTimeframeLengthInMinutes: number,
|
||||
@inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD) override itemOperationsAbuseThreshold: number,
|
||||
@inject(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD)
|
||||
override freeUsersItemOperationsAbuseThreshold: number,
|
||||
@inject(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD) override payloadSizeAbuseThreshold: number,
|
||||
@inject(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD) override freeUsersPayloadSizeAbuseThreshold: number,
|
||||
@inject(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
|
||||
override payloadSizeAbuseTimeframeLengthInMinutes: number,
|
||||
) {
|
||||
super(syncItems, checkIntegrity, getItem, itemHttpMapper, syncResponseFactoryResolver)
|
||||
super(
|
||||
checkForTrafficAbuse,
|
||||
syncItems,
|
||||
checkIntegrity,
|
||||
getItem,
|
||||
itemHttpMapper,
|
||||
syncResponseFactoryResolver,
|
||||
logger,
|
||||
strictAbuseProtection,
|
||||
itemOperationsAbuseTimeframeLengthInMinutes,
|
||||
itemOperationsAbuseThreshold,
|
||||
freeUsersItemOperationsAbuseThreshold,
|
||||
payloadSizeAbuseThreshold,
|
||||
freeUsersPayloadSizeAbuseThreshold,
|
||||
payloadSizeAbuseTimeframeLengthInMinutes,
|
||||
)
|
||||
}
|
||||
|
||||
@httpPost('/sync')
|
||||
|
||||
@@ -11,14 +11,27 @@ import { ApiVersion } from '../../../Domain/Api/ApiVersion'
|
||||
import { SyncItems } from '../../../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { ItemHttpRepresentation } from '../../../Mapping/Http/ItemHttpRepresentation'
|
||||
import { ItemHash } from '../../../Domain/Item/ItemHash'
|
||||
import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
||||
import { Metric } from '../../../Domain/Metrics/Metric'
|
||||
import { Logger } from 'winston'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseItemsController extends BaseHttpController {
|
||||
constructor(
|
||||
protected checkForTrafficAbuse: CheckForTrafficAbuse,
|
||||
protected syncItems: SyncItems,
|
||||
protected checkIntegrity: CheckIntegrity,
|
||||
protected getItem: GetItem,
|
||||
protected itemHttpMapper: MapperInterface<Item, ItemHttpRepresentation>,
|
||||
protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
|
||||
protected logger: Logger,
|
||||
protected strictAbuseProtection: boolean,
|
||||
protected itemOperationsAbuseTimeframeLengthInMinutes: number,
|
||||
protected itemOperationsAbuseThreshold: number,
|
||||
protected freeUsersItemOperationsAbuseThreshold: number,
|
||||
protected payloadSizeAbuseThreshold: number,
|
||||
protected freeUsersPayloadSizeAbuseThreshold: number,
|
||||
protected payloadSizeAbuseTimeframeLengthInMinutes: number,
|
||||
private controllerContainer?: ControllerContainerInterface,
|
||||
) {
|
||||
super()
|
||||
@@ -31,12 +44,60 @@ export class BaseItemsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async sync(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
|
||||
metricToCheck: Metric.NAMES.ItemOperation,
|
||||
userUuid: locals.user.uuid,
|
||||
threshold: locals.isFreeUser ? this.freeUsersItemOperationsAbuseThreshold : this.itemOperationsAbuseThreshold,
|
||||
timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
|
||||
})
|
||||
if (checkForItemOperationsAbuseResult.isFailed()) {
|
||||
this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
|
||||
userId: locals.user.uuid,
|
||||
})
|
||||
if (this.strictAbuseProtection) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'You have exceeded the maximum bandwidth allotted to your account in a 5-minute period. Please wait to try again, or upgrade your account for increased limits.',
|
||||
},
|
||||
},
|
||||
429,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
|
||||
metricToCheck: Metric.NAMES.ContentSizeUtilized,
|
||||
userUuid: locals.user.uuid,
|
||||
threshold: locals.isFreeUser ? this.freeUsersPayloadSizeAbuseThreshold : this.payloadSizeAbuseThreshold,
|
||||
timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
|
||||
})
|
||||
if (checkForPayloadSizeAbuseResult.isFailed()) {
|
||||
this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
|
||||
userId: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (this.strictAbuseProtection) {
|
||||
return this.json(
|
||||
{
|
||||
error: {
|
||||
message:
|
||||
'You have exceeded the maximum bandwidth allotted to your account in a 5-minute period. Please wait to try again, or upgrade your account for increased limits.',
|
||||
},
|
||||
},
|
||||
429,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const itemHashes: ItemHash[] = []
|
||||
if ('items' in request.body) {
|
||||
for (const itemHashInput of request.body.items) {
|
||||
const itemHashOrError = ItemHash.create({
|
||||
...itemHashInput,
|
||||
user_uuid: response.locals.user.uuid,
|
||||
user_uuid: locals.user.uuid,
|
||||
key_system_identifier: itemHashInput.key_system_identifier ?? null,
|
||||
shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null,
|
||||
})
|
||||
@@ -58,7 +119,7 @@ export class BaseItemsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
const syncResult = await this.syncItems.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
itemHashes,
|
||||
computeIntegrityHash: request.body.compute_integrity === true,
|
||||
syncToken: request.body.sync_token,
|
||||
@@ -67,10 +128,10 @@ export class BaseItemsController extends BaseHttpController {
|
||||
contentType: request.body.content_type,
|
||||
apiVersion: request.body.api ?? ApiVersion.v20161215,
|
||||
snjsVersion: <string>request.headers['x-snjs-version'],
|
||||
readOnlyAccess: response.locals.readOnlyAccess,
|
||||
sessionUuid: response.locals.session ? response.locals.session.uuid : null,
|
||||
readOnlyAccess: locals.readOnlyAccess,
|
||||
sessionUuid: locals.session ? locals.session.uuid : null,
|
||||
sharedVaultUuids,
|
||||
isFreeUser: response.locals.isFreeUser,
|
||||
isFreeUser: locals.isFreeUser,
|
||||
})
|
||||
if (syncResult.isFailed()) {
|
||||
return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)
|
||||
@@ -84,13 +145,15 @@ export class BaseItemsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
let integrityPayloads = []
|
||||
if ('integrityPayloads' in request.body) {
|
||||
integrityPayloads = request.body.integrityPayloads
|
||||
}
|
||||
|
||||
const result = await this.checkIntegrity.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
integrityPayloads,
|
||||
})
|
||||
|
||||
@@ -104,8 +167,10 @@ export class BaseItemsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getItem.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
itemUuid: request.params.uuid,
|
||||
})
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SendMessageToUser } from '../../../Domain/UseCase/Messaging/SendMessage
|
||||
import { DeleteAllMessagesSentToUser } from '../../../Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser'
|
||||
import { DeleteMessage } from '../../../Domain/UseCase/Messaging/DeleteMessage/DeleteMessage'
|
||||
import { GetMessagesSentByUser } from '../../../Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseMessagesController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -32,8 +33,10 @@ export class BaseMessagesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getMessages(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getMessageSentToUserUseCase.execute({
|
||||
recipientUuid: response.locals.user.uuid,
|
||||
recipientUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -53,8 +56,10 @@ export class BaseMessagesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getMessagesSent(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getMessagesSentByUserUseCase.execute({
|
||||
senderUuid: response.locals.user.uuid,
|
||||
senderUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -74,8 +79,10 @@ export class BaseMessagesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async sendMessage(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.sendMessageToUserUseCase.execute({
|
||||
senderUuid: response.locals.user.uuid,
|
||||
senderUuid: locals.user.uuid,
|
||||
recipientUuid: request.body.recipient_uuid,
|
||||
encryptedMessage: request.body.encrypted_message,
|
||||
replaceabilityIdentifier: request.body.replaceability_identifier,
|
||||
@@ -98,8 +105,10 @@ export class BaseMessagesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteMessagesSentToUser(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.deleteMessagesSentToUserUseCase.execute({
|
||||
recipientUuid: response.locals.user.uuid,
|
||||
recipientUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -117,9 +126,11 @@ export class BaseMessagesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteMessage(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.deleteMessageUseCase.execute({
|
||||
messageUuid: request.params.messageUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
originatorUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DeleteSharedVaultInvitesToUser } from '../../../Domain/UseCase/SharedVa
|
||||
import { GetSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser'
|
||||
import { DeleteSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser'
|
||||
import { GetSharedVaultInvitesSentToUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -63,9 +64,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async createSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.inviteUserToSharedVaultUseCase.execute({
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
senderUuid: response.locals.user.uuid,
|
||||
senderUuid: locals.user.uuid,
|
||||
recipientUuid: request.body.recipient_uuid,
|
||||
encryptedMessage: request.body.encrypted_message,
|
||||
permission: request.body.permission,
|
||||
@@ -88,10 +91,12 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async updateSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.updateSharedVaultInviteUseCase.execute({
|
||||
encryptedMessage: request.body.encrypted_message,
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
senderUuid: response.locals.user.uuid,
|
||||
senderUuid: locals.user.uuid,
|
||||
permission: request.body.permission,
|
||||
})
|
||||
|
||||
@@ -112,9 +117,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async acceptSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.acceptSharedVaultInviteUseCase.execute({
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
originatorUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -128,7 +135,7 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.json({
|
||||
success: true,
|
||||
@@ -136,9 +143,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.declineSharedVaultInviteUseCase.execute({
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -158,8 +167,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteInboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.deleteSharedVaultInvitesToUserUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -179,8 +190,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -200,8 +213,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
|
||||
senderUuid: response.locals.user.uuid,
|
||||
senderUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -221,8 +236,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
|
||||
senderUuid: response.locals.user.uuid,
|
||||
senderUuid: locals.user.uuid,
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
})
|
||||
|
||||
@@ -243,8 +260,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getSharedVaultInvitesSentToUserUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -264,9 +283,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.declineSharedVaultInviteUseCase.execute({
|
||||
inviteUuid: request.params.inviteUuid,
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -286,8 +307,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteAllSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedV
|
||||
import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers'
|
||||
import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault'
|
||||
import { DesignateSurvivor } from '../../../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -29,8 +30,10 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSharedVaultUsers(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.getSharedVaultUsersUseCase.execute({
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
originatorUuid: locals.user.uuid,
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
})
|
||||
|
||||
@@ -51,10 +54,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async removeUserFromSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.removeUserFromSharedVaultUseCase.execute({
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
userUuid: request.params.userUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
originatorUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -76,10 +81,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async designateSurvivor(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.designateSurvivorUseCase.execute({
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
userUuid: request.params.userUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
originatorUuid: locals.user.uuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CreateSharedVault } from '../../../Domain/UseCase/SharedVaults/CreateSh
|
||||
import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation'
|
||||
import { DeleteSharedVault } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault'
|
||||
import { CreateSharedVaultFileValetToken } from '../../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class BaseSharedVaultsController extends BaseHttpController {
|
||||
constructor(
|
||||
@@ -36,8 +37,10 @@ export class BaseSharedVaultsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async getSharedVaults(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const resultOrError = await this.getSharedVaultsUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
includeDesignatedSurvivors: true,
|
||||
})
|
||||
|
||||
@@ -64,9 +67,11 @@ export class BaseSharedVaultsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async createSharedVault(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.createSharedVaultUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userRoleNames: response.locals.roles.map((role: Role) => role.name),
|
||||
userUuid: locals.user.uuid,
|
||||
userRoleNames: locals.roles.map((role: Role) => role.name),
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
@@ -80,7 +85,7 @@ export class BaseSharedVaultsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.json({
|
||||
sharedVault: this.sharedVaultHttpMapper.toProjection(result.getValue().sharedVault),
|
||||
@@ -89,9 +94,11 @@ export class BaseSharedVaultsController extends BaseHttpController {
|
||||
}
|
||||
|
||||
async deleteSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.deleteSharedVaultUseCase.execute({
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
originatorUuid: response.locals.user.uuid,
|
||||
originatorUuid: locals.user.uuid,
|
||||
allowSurviving: false,
|
||||
})
|
||||
|
||||
@@ -106,16 +113,18 @@ export class BaseSharedVaultsController extends BaseHttpController {
|
||||
)
|
||||
}
|
||||
|
||||
response.setHeader('x-invalidate-cache', response.locals.user.uuid)
|
||||
response.setHeader('x-invalidate-cache', locals.user.uuid)
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
async createValetTokenForSharedVaultFile(request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const locals = response.locals as ResponseLocals
|
||||
|
||||
const result = await this.createSharedVaultFileValetTokenUseCase.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
userUuid: locals.user.uuid,
|
||||
sharedVaultUuid: request.params.sharedVaultUuid,
|
||||
sharedVaultOwnerUploadBytesLimit: response.locals.sharedVaultOwnerContext?.upload_bytes_limit,
|
||||
sharedVaultOwnerUploadBytesLimit: locals.sharedVaultOwnerContext?.upload_bytes_limit,
|
||||
fileUuid: request.body.file_uuid,
|
||||
remoteIdentifier: request.body.remote_identifier,
|
||||
operation: request.body.operation,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { verify } from 'jsonwebtoken'
|
||||
import { CrossServiceTokenData } from '@standardnotes/security'
|
||||
import * as winston from 'winston'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { ResponseLocals } from '../ResponseLocals'
|
||||
|
||||
export class InversifyExpressAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@@ -25,13 +26,14 @@ export class InversifyExpressAuthMiddleware extends BaseMiddleware {
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(authToken, this.authJWTSecret, { algorithms: ['HS256'] })
|
||||
|
||||
response.locals.user = decodedToken.user
|
||||
response.locals.roles = decodedToken.roles
|
||||
response.locals.isFreeUser =
|
||||
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
|
||||
response.locals.session = decodedToken.session
|
||||
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
|
||||
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
|
||||
Object.assign(response.locals, {
|
||||
user: decodedToken.user,
|
||||
roles: decodedToken.roles,
|
||||
isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
|
||||
session: decodedToken.session,
|
||||
readOnlyAccess: decodedToken.session?.readonly_access ?? false,
|
||||
sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
|
||||
} as ResponseLocals)
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Role } from '@standardnotes/security'
|
||||
|
||||
export interface ResponseLocals {
|
||||
user: {
|
||||
uuid: string
|
||||
email: string
|
||||
}
|
||||
roles: Array<Role>
|
||||
isFreeUser: boolean
|
||||
session?: {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
readOnlyAccess: boolean
|
||||
sharedVaultOwnerContext?: {
|
||||
upload_bytes_limit: number
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,142 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterface'
|
||||
import { Metric } from '../../Domain/Metrics/Metric'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
export class RedisMetricStore implements MetricsStoreInterface {
|
||||
private readonly METRIC_PREFIX = 'metric'
|
||||
private readonly METRIC_PER_USER_PREFIX = 'metric-user'
|
||||
|
||||
constructor(
|
||||
private redisClient: IORedis.Redis,
|
||||
private timer: TimerInterface,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
async getStatistics(
|
||||
name: string,
|
||||
from: number,
|
||||
to: number,
|
||||
): Promise<{ sum: number; max: number; min: number; sampleCount: number }> {
|
||||
async getUserBasedMetricsSummaryWithinTimeRange(dto: {
|
||||
metricName: string
|
||||
userUuid: Uuid
|
||||
from: Date
|
||||
to: Date
|
||||
}): Promise<MetricsSummary> {
|
||||
this.logger.debug(`Fetching user based metrics summary for ${dto.metricName}.`, {
|
||||
codeTag: 'RedisMetricStore',
|
||||
userId: dto.userUuid.value,
|
||||
from: dto.from.toISOString(),
|
||||
to: dto.to.toISOString(),
|
||||
})
|
||||
|
||||
const keys = this.getKeysRepresentingMinutesBetweenFromAndTo(dto.from, dto.to)
|
||||
|
||||
this.logger.debug(`Fetching user based metrics summary for ${dto.metricName} - keys: ${keys.join(', ')}.`, {
|
||||
codeTag: 'RedisMetricStore',
|
||||
userId: dto.userUuid.value,
|
||||
})
|
||||
|
||||
let sum = 0
|
||||
let max = 0
|
||||
let min = 0
|
||||
let sampleCount = 0
|
||||
|
||||
const values = await this.redisClient.mget(
|
||||
keys.map((key) => `${this.METRIC_PER_USER_PREFIX}:${dto.userUuid.value}:${dto.metricName}:${key}`),
|
||||
)
|
||||
|
||||
this.logger.debug(`Fetching user based metrics summary for ${dto.metricName} - values: ${values.join(', ')}.`, {
|
||||
codeTag: 'RedisMetricStore',
|
||||
userId: dto.userUuid.value,
|
||||
})
|
||||
|
||||
for (const value of values) {
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
|
||||
const valueAsNumber = Number(value)
|
||||
|
||||
sum += valueAsNumber
|
||||
sampleCount++
|
||||
|
||||
if (valueAsNumber > max) {
|
||||
max = valueAsNumber
|
||||
}
|
||||
|
||||
if (valueAsNumber < min) {
|
||||
min = valueAsNumber
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sum,
|
||||
max,
|
||||
min,
|
||||
sampleCount,
|
||||
}
|
||||
}
|
||||
|
||||
async storeUserBasedMetric(metric: Metric, value: number, userUuid: Uuid): Promise<void> {
|
||||
const date = this.timer.convertMicrosecondsToDate(metric.props.timestamp)
|
||||
const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
|
||||
const key = `${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${metric.props.name}:${dateToTheMinuteString}`
|
||||
const itemOperationKey = `${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${Metric.NAMES.ItemOperation}:${dateToTheMinuteString}`
|
||||
|
||||
const pipeline = this.redisClient.pipeline()
|
||||
|
||||
pipeline.incrbyfloat(key, value)
|
||||
pipeline.incr(itemOperationKey)
|
||||
|
||||
const expirationTime = 60 * 60 * 24
|
||||
pipeline.expire(key, expirationTime)
|
||||
pipeline.expire(itemOperationKey, expirationTime)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async getUserBasedMetricsSummary(name: string, timestamp: number): Promise<MetricsSummary> {
|
||||
const date = this.timer.convertMicrosecondsToDate(timestamp)
|
||||
const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm')
|
||||
|
||||
const userMetricsKeys = await this.redisClient.keys(
|
||||
`${this.METRIC_PER_USER_PREFIX}:*:${name}:${dateToTheMinuteString}`,
|
||||
)
|
||||
|
||||
let sum = 0
|
||||
let max = 0
|
||||
let min = 0
|
||||
let sampleCount = 0
|
||||
|
||||
const values = await this.redisClient.mget(userMetricsKeys)
|
||||
|
||||
for (const value of values) {
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
|
||||
const valueAsNumber = Number(value)
|
||||
|
||||
sum += valueAsNumber
|
||||
sampleCount++
|
||||
|
||||
if (valueAsNumber > max) {
|
||||
max = valueAsNumber
|
||||
}
|
||||
|
||||
if (valueAsNumber < min) {
|
||||
min = valueAsNumber
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sum,
|
||||
max,
|
||||
min,
|
||||
sampleCount,
|
||||
}
|
||||
}
|
||||
|
||||
async getMetricsSummary(name: string, from: number, to: number): Promise<MetricsSummary> {
|
||||
const keysRepresentingSecondsBetweenFromAndTo = this.getKeysRepresentingSecondsBetweenFromAndTo(from, to)
|
||||
|
||||
let sum = 0
|
||||
@@ -64,12 +186,28 @@ export class RedisMetricStore implements MetricsStoreInterface {
|
||||
|
||||
pipeline.incr(key)
|
||||
|
||||
const expirationTimeIn24Hours = 60 * 60 * 24
|
||||
pipeline.expire(key, expirationTimeIn24Hours)
|
||||
const expirationTime = 60 * 60 * 6
|
||||
pipeline.expire(key, expirationTime)
|
||||
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
private getKeysRepresentingMinutesBetweenFromAndTo(from: Date, to: Date): string[] {
|
||||
const keys: string[] = []
|
||||
|
||||
let currentMinute = from
|
||||
|
||||
while (currentMinute <= to) {
|
||||
const dateToTheMinuteString = this.timer.convertDateToFormattedString(currentMinute, 'YYYY-MM-DD HH:mm')
|
||||
|
||||
keys.push(dateToTheMinuteString)
|
||||
|
||||
currentMinute = new Date(currentMinute.getTime() + 60 * 1000)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
private getKeysRepresentingSecondsBetweenFromAndTo(from: number, to: number): string[] {
|
||||
const keys: string[] = []
|
||||
|
||||
|
||||
@@ -9,12 +9,22 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
|
||||
import { ApiVersion } from '../../Domain/Api/ApiVersion'
|
||||
import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
|
||||
import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
|
||||
import { CheckForTrafficAbuse } from '../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
|
||||
import { Metric } from '../../Domain/Metrics/Metric'
|
||||
|
||||
export class SyncingServer implements ISyncingServer {
|
||||
constructor(
|
||||
private syncItemsUseCase: SyncItems,
|
||||
private syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
|
||||
private mapper: MapperInterface<SyncResponse20200115, SyncResponse>,
|
||||
protected checkForTrafficAbuse: CheckForTrafficAbuse,
|
||||
private strictAbuseProtection: boolean,
|
||||
private itemOperationsAbuseTimeframeLengthInMinutes: number,
|
||||
private itemOperationsAbuseThreshold: number,
|
||||
private freeUsersItemOperationsAbuseThreshold: number,
|
||||
private payloadSizeAbuseThreshold: number,
|
||||
private freeUsersPayloadSizeAbuseThreshold: number,
|
||||
private payloadSizeAbuseTimeframeLengthInMinutes: number,
|
||||
private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -23,7 +33,69 @@ export class SyncingServer implements ISyncingServer {
|
||||
callback: grpc.sendUnaryData<SyncResponse>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.debug('[SyncingServer] Syncing items via gRPC')
|
||||
const userUuid = call.metadata.get('x-user-uuid').pop() as string
|
||||
const isFreeUser = call.metadata.get('x-is-free-user').pop() === 'true'
|
||||
|
||||
const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
|
||||
metricToCheck: Metric.NAMES.ItemOperation,
|
||||
userUuid,
|
||||
threshold: isFreeUser ? this.freeUsersItemOperationsAbuseThreshold : this.itemOperationsAbuseThreshold,
|
||||
timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
|
||||
})
|
||||
if (checkForItemOperationsAbuseResult.isFailed()) {
|
||||
this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
|
||||
userId: userUuid,
|
||||
})
|
||||
if (this.strictAbuseProtection) {
|
||||
const metadata = new grpc.Metadata()
|
||||
metadata.set(
|
||||
'x-sync-error-message',
|
||||
'You have exceeded the maximum bandwidth allotted to your account in a 5-minute period. Please wait to try again, or upgrade your account for increased limits.',
|
||||
)
|
||||
metadata.set('x-sync-error-response-code', '429')
|
||||
|
||||
return callback(
|
||||
{
|
||||
code: Status.INVALID_ARGUMENT,
|
||||
message: checkForItemOperationsAbuseResult.getError(),
|
||||
name: 'INVALID_ARGUMENT',
|
||||
metadata,
|
||||
},
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
|
||||
metricToCheck: Metric.NAMES.ContentSizeUtilized,
|
||||
userUuid,
|
||||
threshold: isFreeUser ? this.freeUsersPayloadSizeAbuseThreshold : this.payloadSizeAbuseThreshold,
|
||||
timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
|
||||
})
|
||||
if (checkForPayloadSizeAbuseResult.isFailed()) {
|
||||
this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
|
||||
userId: userUuid,
|
||||
})
|
||||
|
||||
if (this.strictAbuseProtection) {
|
||||
const metadata = new grpc.Metadata()
|
||||
metadata.set(
|
||||
'x-sync-error-message',
|
||||
'You have exceeded the maximum bandwidth allotted to your account in a 5-minute period. Please wait to try again, or upgrade your account for increased limits.',
|
||||
)
|
||||
metadata.set('x-sync-error-response-code', '429')
|
||||
|
||||
return callback(
|
||||
{
|
||||
code: Status.INVALID_ARGUMENT,
|
||||
message: checkForPayloadSizeAbuseResult.getError(),
|
||||
name: 'INVALID_ARGUMENT',
|
||||
metadata,
|
||||
},
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const itemHashesRPC = call.request.getItemsList()
|
||||
const itemHashes: ItemHash[] = []
|
||||
@@ -41,7 +113,7 @@ export class SyncingServer implements ISyncingServer {
|
||||
created_at_timestamp: itemHash.hasCreatedAtTimestamp() ? itemHash.getCreatedAtTimestamp() : undefined,
|
||||
updated_at: itemHash.hasUpdatedAt() ? itemHash.getUpdatedAt() : undefined,
|
||||
updated_at_timestamp: itemHash.hasUpdatedAtTimestamp() ? itemHash.getUpdatedAtTimestamp() : undefined,
|
||||
user_uuid: call.metadata.get('userUuid').pop() as string,
|
||||
user_uuid: userUuid,
|
||||
key_system_identifier: itemHash.hasKeySystemIdentifier()
|
||||
? (itemHash.getKeySystemIdentifier() as string)
|
||||
: null,
|
||||
@@ -74,7 +146,6 @@ export class SyncingServer implements ISyncingServer {
|
||||
}
|
||||
|
||||
const apiVersion = call.request.hasApiVersion() ? (call.request.getApiVersion() as string) : ApiVersion.v20161215
|
||||
const userUuid = call.metadata.get('x-user-uuid').pop() as string
|
||||
const readOnlyAccess = call.metadata.get('x-read-only-access').pop() === 'true'
|
||||
if (readOnlyAccess) {
|
||||
this.logger.debug('Syncing with read-only access', {
|
||||
@@ -96,7 +167,7 @@ export class SyncingServer implements ISyncingServer {
|
||||
readOnlyAccess,
|
||||
sessionUuid: call.metadata.get('x-session-uuid').pop() as string,
|
||||
sharedVaultUuids,
|
||||
isFreeUser: call.metadata.get('x-is-free-user').pop() === 'true',
|
||||
isFreeUser,
|
||||
})
|
||||
if (syncResult.isFailed()) {
|
||||
const metadata = new grpc.Metadata()
|
||||
|
||||
Reference in New Issue
Block a user