Compare commits

...

22 Commits

Author SHA1 Message Date
standardci cad28ebba5 chore(release): publish new version
- @standardnotes/auth-server@1.177.19
 - @standardnotes/home-server@1.22.60
2024-01-17 12:54:44 +00:00
Karol Sójko 460fdf9eaf fix(auth): add server daily email backup permission for all versions of core user role (#1028) 2024-01-17 13:34:00 +01:00
standardci bec1b502ad chore(release): publish new version
- @standardnotes/home-server@1.22.59
 - @standardnotes/syncing-server@1.135.0
2024-01-17 10:48:28 +00:00
Karol Sójko 70bbf11db5 feat(syncing-server): add procedure to recalculate content sizes (#1027) 2024-01-17 11:27:26 +01:00
standardci c00c7becae chore(release): publish new version
- @standardnotes/home-server@1.22.58
 - @standardnotes/syncing-server@1.134.1
2024-01-16 10:41:23 +00:00
Karol Sójko 89dc6c19bf fix(syncing-server): missing item operations metric store expiry 2024-01-16 11:20:35 +01:00
standardci 972a91d59f chore(release): publish new version
- @standardnotes/auth-server@1.177.18
 - @standardnotes/home-server@1.22.57
2024-01-15 12:09:42 +00:00
Karol Sójko 045358ddbf fix(auth): add renewal for shared offline subscriptions 2024-01-15 12:42:54 +01:00
Karol Sójko c7217a92ba fix(auth): add more logs to syncing subscription 2024-01-15 12:39:27 +01:00
standardci 3da7a21cde chore(release): publish new version
- @standardnotes/auth-server@1.177.17
 - @standardnotes/home-server@1.22.56
2024-01-15 10:27:24 +00:00
Karol Sójko 351e18f638 fix(auth): add debug logs for subscription sync requested event 2024-01-15 11:06:19 +01:00
standardci 4f2129c4e0 chore(release): publish new version
- @standardnotes/auth-server@1.177.16
 - @standardnotes/home-server@1.22.55
2024-01-15 09:44:23 +00:00
Karol Sójko d7a1c667dd fix(auth): update shared subscriptions upon subscription sync (#1022) 2024-01-15 10:23:51 +01:00
standardci 4de0bfa36d chore(release): publish new version
- @standardnotes/home-server@1.22.54
 - @standardnotes/syncing-server@1.134.0
2024-01-12 15:06:26 +00:00
Karol Sójko 0443de88ce feat(syncing-server): reduced abuse thresholds for free users (#1021) 2024-01-12 15:45:00 +01:00
Karol Sójko f830bac873 fix: reduce the transfer limit on e2e tests (#1020) 2024-01-11 13:12:33 +01:00
standardci 517ae5ded9 chore(release): publish new version
- @standardnotes/api-gateway@1.89.19
 - @standardnotes/files-server@1.37.9
 - @standardnotes/home-server@1.22.53
 - @standardnotes/syncing-server@1.133.6
2024-01-10 14:40:54 +00:00
Karol Sójko 6062f85000 fix: add dedicated http code response upon a request with too large payload (#1019)
* fix: add dedicated http code response upon a request with too large payload

* fix error log
2024-01-10 15:19:26 +01:00
standardci e2205c3849 chore(release): publish new version
- @standardnotes/auth-server@1.177.15
 - @standardnotes/home-server@1.22.52
2024-01-09 09:17:45 +00:00
Karol Sójko 0b46eff16e fix(auth): check for user agent persisting on session during a session refresh (#1016) 2024-01-09 09:57:02 +01:00
standardci df67982bca chore(release): publish new version
- @standardnotes/api-gateway@1.89.18
 - @standardnotes/auth-server@1.177.14
 - @standardnotes/home-server@1.22.51
 - @standardnotes/syncing-server@1.133.5
2024-01-08 12:25:14 +00:00
Karol Sójko d44866b3c0 chore: add types for response locals (#1015) 2024-01-08 13:04:25 +01:00
79 changed files with 1172 additions and 317 deletions
+1 -1
View File
@@ -27,4 +27,4 @@ AUTH_JWT_SECRET=f95259c5e441f5a4646d76422cfb3df4c4488842901aa50b6c51b8be2e0040e9
AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060 AUTH_SERVER_ENCRYPTION_SERVER_KEY=1087415dfde3093797f9a7ca93a49e7d7aa1861735eb0d32aae9c303b8c3d060
VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f VALET_TOKEN_SECRET=4b886819ebe1e908077c6cae96311b48a8416bd60cc91c03060e15bdf6b30d1f
SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=1000000 SYNCING_SERVER_CONTENT_SIZE_TRANSFER_LIMIT=100000
+1 -1
View File
@@ -70,7 +70,7 @@ jobs:
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env echo "REFRESH_TOKEN_AGE=10" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=2" >> 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_HOST=localhost" >> packages/home-server/.env
echo "DB_PORT=3306" >> packages/home-server/.env echo "DB_PORT=3306" >> packages/home-server/.env
echo "DB_DATABASE=standardnotes" >> packages/home-server/.env echo "DB_DATABASE=standardnotes" >> packages/home-server/.env
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 ### Bug Fixes
+19 -2
View File
@@ -36,12 +36,17 @@ import { InversifyExpressServer } from 'inversify-express-utils'
import { ContainerConfigLoader } from '../src/Bootstrap/Container' import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import { TYPES } from '../src/Bootstrap/Types' import { TYPES } from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env' import { Env } from '../src/Bootstrap/Env'
import { ResponseLocals } from '../src/Controller/ResponseLocals'
const container = new ContainerConfigLoader() const container = new ContainerConfigLoader()
void container.load().then((container) => { void container.load().then((container) => {
const env: Env = new Env() const env: Env = new Env()
env.load() 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) const server = new InversifyExpressServer(container)
server.setConfig((app) => { server.setConfig((app) => {
@@ -72,7 +77,7 @@ void container.load().then((container) => {
}), }),
) )
app.use(json({ limit: '50mb' })) app.use(json({ limit: requestPayloadLimit }))
app.use( app.use(
text({ text({
type: ['text/plain', 'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded; charset=utf-8'], 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) => { server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => { app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
const locals = response.locals as ResponseLocals
logger.error(`${error.stack}`, { logger.error(`${error.stack}`, {
method: request.method, method: request.method,
url: request.url, url: request.url,
snjs: request.headers['x-snjs-version'], snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-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( logger.debug(
`[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${ `[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)}`, }] 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({ response.status(500).send({
error: { error: {
message: message:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/api-gateway", "name": "@standardnotes/api-gateway",
"version": "1.89.17", "version": "1.89.19",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
@@ -8,6 +8,8 @@ import { Logger } from 'winston'
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface' import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface' import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
import { ResponseLocals } from './ResponseLocals'
import { RoleName } from '@standardnotes/domain-core'
export abstract class AuthMiddleware extends BaseMiddleware { export abstract class AuthMiddleware extends BaseMiddleware {
constructor( constructor(
@@ -55,33 +57,27 @@ export abstract class AuthMiddleware extends BaseMiddleware {
crossServiceTokenFetchedFromCache = false crossServiceTokenFetchedFromCache = false
} }
response.locals.authToken = crossServiceToken const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
const decodedToken = <CrossServiceTokenData>(
verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
)
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) { if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
await this.crossServiceTokenCache.set({ await this.crossServiceTokenCache.set({
key: cacheKey, key: cacheKey,
encodedCrossServiceToken: response.locals.authToken, encodedCrossServiceToken: crossServiceToken,
expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken), expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
userUuid: decodedToken.user.uuid, userUuid: decodedToken.user.uuid,
}) })
} }
response.locals.user = decodedToken.user Object.assign(response.locals, {
response.locals.session = decodedToken.session authToken: crossServiceToken,
response.locals.roles = decodedToken.roles user: decodedToken.user,
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context session: decodedToken.session,
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false roles: decodedToken.roles,
if (response.locals.readOnlyAccess) { sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
this.logger.debug('User operates on read-only access', { readOnlyAccess: decodedToken.session?.readonly_access ?? false,
codeTag: 'AuthMiddleware', isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
userId: response.locals.user.uuid, belongsToSharedVaults: decodedToken.belongs_to_shared_vaults ?? [],
}) } as ResponseLocals)
}
response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
} catch (error) { } catch (error) {
let detailedErrorMessage = (error as Error).message let detailedErrorMessage = (error as Error).message
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
@@ -6,6 +6,7 @@ import { verify } from 'jsonwebtoken'
import { Logger } from 'winston' import { Logger } from 'winston'
import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc' import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc'
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
import { ResponseLocals } from './ResponseLocals'
export class GRPCWebSocketAuthMiddleware extends BaseMiddleware { export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
constructor( constructor(
@@ -90,15 +91,16 @@ export class GRPCWebSocketAuthMiddleware extends BaseMiddleware {
const crossServiceToken = authResponse.data.authToken as string const crossServiceToken = authResponse.data.authToken as string
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] }) const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user Object.assign(response.locals, {
response.locals.session = decodedToken.session authToken: crossServiceToken,
response.locals.roles = decodedToken.roles user: decodedToken.user,
response.locals.isFreeUser = session: decodedToken.session,
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser 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) { } catch (error) {
this.logger.error( this.logger.error(
`Could not pass the request to websocket connection validation on underlying service: ${ `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
}
@@ -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 { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types' import { TYPES } from '../Bootstrap/Types'
import { TokenAuthenticationMethod } from './TokenAuthenticationMethod' import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
import { ResponseLocals } from './ResponseLocals'
import { OfflineResponseLocals } from './OfflineResponseLocals'
import { SubscriptionResponseLocals } from './SubscriptionResponseLocals'
@injectable() @injectable()
export class SubscriptionTokenAuthMiddleware extends BaseMiddleware { export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
@@ -34,13 +37,16 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
return return
} }
response.locals.tokenAuthenticationMethod = email const locals = {
? TokenAuthenticationMethod.OfflineSubscriptionToken tokenAuthenticationMethod: email
: TokenAuthenticationMethod.SubscriptionToken ? TokenAuthenticationMethod.OfflineSubscriptionToken
: TokenAuthenticationMethod.SubscriptionToken,
} as SubscriptionResponseLocals
Object.assign(response.locals, locals)
try { try {
const url = const url =
response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate` ? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate`
: `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate` : `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate`
@@ -65,7 +71,7 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
return return
} }
if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) { if (locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
this.handleOfflineAuthTokenValidationResponse(response, authResponse) this.handleOfflineAuthTokenValidationResponse(response, authResponse)
return next() return next()
@@ -101,24 +107,26 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
} }
private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) { private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
response.locals.offlineAuthToken = authResponse.data.authToken
const decodedToken = <OfflineUserTokenData>( const decodedToken = <OfflineUserTokenData>(
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] }) verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
) )
response.locals.offlineUserEmail = decodedToken.userEmail Object.assign(response.locals, {
response.locals.offlineFeaturesToken = decodedToken.featuresToken offlineAuthToken: authResponse.data.authToken,
userEmail: decodedToken.userEmail,
featuresToken: decodedToken.featuresToken,
} as OfflineResponseLocals)
} }
private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) { private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
response.locals.authToken = authResponse.data.authToken
const decodedToken = <CrossServiceTokenData>( const decodedToken = <CrossServiceTokenData>(
verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] }) verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
) )
response.locals.user = decodedToken.user Object.assign(response.locals, {
response.locals.roles = decodedToken.roles 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 { Logger } from 'winston'
import { TYPES } from '../Bootstrap/Types' import { TYPES } from '../Bootstrap/Types'
import { ResponseLocals } from './ResponseLocals'
@injectable() @injectable()
export class WebSocketAuthMiddleware extends BaseMiddleware { export class WebSocketAuthMiddleware extends BaseMiddleware {
@@ -55,13 +56,14 @@ export class WebSocketAuthMiddleware extends BaseMiddleware {
const crossServiceToken = authResponse.data.authToken const crossServiceToken = authResponse.data.authToken
response.locals.authToken = crossServiceToken
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] }) const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user Object.assign(response.locals, {
response.locals.session = decodedToken.session authToken: crossServiceToken,
response.locals.roles = decodedToken.roles user: decodedToken.user,
session: decodedToken.session,
roles: decodedToken.roles,
} as ResponseLocals)
} catch (error) { } catch (error) {
const errorMessage = (error as AxiosError).isAxiosError const errorMessage = (error as AxiosError).isAxiosError
? JSON.stringify((error as AxiosError).response?.data) ? JSON.stringify((error as AxiosError).response?.data)
@@ -16,6 +16,8 @@ import { TYPES } from '../../Bootstrap/Types'
import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface' import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod' import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface' import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
import { ResponseLocals } from '../ResponseLocals'
import { SubscriptionResponseLocals } from '../SubscriptionResponseLocals'
@controller('/v1/users') @controller('/v1/users')
export class UsersController extends BaseHttpController { export class UsersController extends BaseHttpController {
@@ -214,7 +216,9 @@ export class UsersController extends BaseHttpController {
@httpGet('/subscription', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware) @httpGet('/subscription', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware)
async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise<void> { 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( await this.httpService.callAuthServer(
request, request,
response, response,
@@ -227,11 +231,7 @@ export class UsersController extends BaseHttpController {
await this.httpService.callAuthServer( await this.httpService.callAuthServer(
request, request,
response, response,
this.endpointResolver.resolveEndpointOrMethodIdentifier( this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/:userUuid/subscription', locals.user.uuid),
'GET',
'users/:userUuid/subscription',
response.locals.user.uuid,
),
) )
} }
@@ -2,6 +2,7 @@ import { Request, Response } from 'express'
import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core' import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface' import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
import { ResponseLocals } from '../../Controller/ResponseLocals'
export class DirectCallServiceProxy implements ServiceProxyInterface { export class DirectCallServiceProxy implements ServiceProxyInterface {
constructor( constructor(
@@ -134,11 +135,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
response: Response, response: Response,
serviceResponse: { statusCode: number; json: Record<string, unknown> }, serviceResponse: { statusCode: number; json: Record<string, unknown> },
): void { ): void {
const locals = response.locals as ResponseLocals
void response.status(serviceResponse.statusCode).send({ void response.status(serviceResponse.statusCode).send({
meta: { meta: {
auth: { auth: {
userUuid: response.locals.user?.uuid, userUuid: locals.user?.uuid,
roles: response.locals.roles, roles: locals.roles,
}, },
server: { server: {
filesServerUrl: this.filesServerUrl, filesServerUrl: this.filesServerUrl,
@@ -8,6 +8,8 @@ import { TYPES } from '../../Bootstrap/Types'
import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface' import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface' import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { ResponseLocals } from '../../Controller/ResponseLocals'
import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals'
@injectable() @injectable()
export class HttpServiceProxy implements ServiceProxyInterface { export class HttpServiceProxy implements ServiceProxyInterface {
@@ -176,6 +178,8 @@ export class HttpServiceProxy implements ServiceProxyInterface {
endpoint: string, endpoint: string,
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
): Promise<AxiosResponse | undefined> { ): Promise<AxiosResponse | undefined> {
const locals = response.locals as ResponseLocals | OfflineResponseLocals
try { try {
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
for (const headerName of Object.keys(request.headers)) { for (const headerName of Object.keys(request.headers)) {
@@ -185,12 +189,12 @@ export class HttpServiceProxy implements ServiceProxyInterface {
delete headers.host delete headers.host
delete headers['content-length'] delete headers['content-length']
if (response.locals.authToken) { if ('authToken' in locals && locals.authToken) {
headers['X-Auth-Token'] = response.locals.authToken headers['X-Auth-Token'] = locals.authToken
} }
if (response.locals.offlineAuthToken) { if ('offlineAuthToken' in locals && locals.offlineAuthToken) {
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken headers['X-Auth-Offline-Token'] = locals.offlineAuthToken
} }
const serviceResponse = await this.httpClient.request({ const serviceResponse = await this.httpClient.request({
@@ -222,7 +226,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
this.logger.error( this.logger.error(
`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,
}, },
) )
@@ -257,6 +261,7 @@ export class HttpServiceProxy implements ServiceProxyInterface {
endpoint: string, endpoint: string,
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
): Promise<void> { ): Promise<void> {
const locals = response.locals as ResponseLocals
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload) const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
if (!serviceResponse) { if (!serviceResponse) {
@@ -274,8 +279,8 @@ export class HttpServiceProxy implements ServiceProxyInterface {
response.status(serviceResponse.status).send({ response.status(serviceResponse.status).send({
meta: { meta: {
auth: { auth: {
userUuid: response.locals.user?.uuid, userUuid: locals.user?.uuid,
roles: response.locals.roles, roles: locals.roles,
}, },
server: { server: {
filesServerUrl: this.filesServerUrl, filesServerUrl: this.filesServerUrl,
@@ -40,7 +40,6 @@ export class EndpointResolver implements EndpointResolverInterface {
// Tokens Controller // Tokens Controller
['[POST]:subscription-tokens', 'auth.subscription-tokens.create'], ['[POST]:subscription-tokens', 'auth.subscription-tokens.create'],
// Users Controller // Users Controller
['[PATCH]:users/:userId', 'auth.users.update'],
['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'], ['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'],
['[DELETE]:users/:userUuid', 'auth.users.delete'], ['[DELETE]:users/:userUuid', 'auth.users.delete'],
['[POST]:listed', 'auth.users.createListedAccount'], ['[POST]:listed', 'auth.users.createListedAccount'],
@@ -9,6 +9,8 @@ import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCache
import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface' import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
import { GRPCSyncingServerServiceProxy } from './GRPCSyncingServerServiceProxy' import { GRPCSyncingServerServiceProxy } from './GRPCSyncingServerServiceProxy'
import { Status } from '@grpc/grpc-js/build/src/constants' import { Status } from '@grpc/grpc-js/build/src/constants'
import { ResponseLocals } from '../../Controller/ResponseLocals'
import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals'
export class GRPCServiceProxy implements ServiceProxyInterface { export class GRPCServiceProxy implements ServiceProxyInterface {
constructor( constructor(
@@ -135,13 +137,15 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
response: Response, response: Response,
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
): Promise<void> { ): Promise<void> {
const locals = response.locals as ResponseLocals
const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload) const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload)
response.status(result.status).send({ response.status(result.status).send({
meta: { meta: {
auth: { auth: {
userUuid: response.locals.user?.uuid, userUuid: locals.user?.uuid,
roles: response.locals.roles, roles: locals.roles,
}, },
server: { server: {
filesServerUrl: this.filesServerUrl, filesServerUrl: this.filesServerUrl,
@@ -250,6 +254,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
retryAttempt?: number, retryAttempt?: number,
): Promise<AxiosResponse | undefined> { ): Promise<AxiosResponse | undefined> {
const locals = response.locals as ResponseLocals | OfflineResponseLocals
try { try {
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
for (const headerName of Object.keys(request.headers)) { for (const headerName of Object.keys(request.headers)) {
@@ -259,12 +265,12 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
delete headers.host delete headers.host
delete headers['content-length'] delete headers['content-length']
if (response.locals.authToken) { if ('authToken' in locals && locals.authToken) {
headers['X-Auth-Token'] = response.locals.authToken headers['X-Auth-Token'] = locals.authToken
} }
if (response.locals.offlineAuthToken) { if ('offlineAuthToken' in locals && locals.offlineAuthToken) {
headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken headers['X-Auth-Offline-Token'] = locals.offlineAuthToken
} }
const serviceResponse = await this.httpClient.request({ const serviceResponse = await this.httpClient.request({
@@ -314,7 +320,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries` ? `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,
}, },
) )
@@ -349,6 +355,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
endpoint: string, endpoint: string,
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
): Promise<void> { ): Promise<void> {
const locals = response.locals as ResponseLocals
const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload) const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
if (!serviceResponse) { if (!serviceResponse) {
@@ -366,8 +374,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface {
response.status(serviceResponse.status).send({ response.status(serviceResponse.status).send({
meta: { meta: {
auth: { auth: {
userUuid: response.locals.user?.uuid, userUuid: locals.user?.uuid,
roles: response.locals.roles, roles: locals.roles,
}, },
server: { server: {
filesServerUrl: this.filesServerUrl, filesServerUrl: this.filesServerUrl,
@@ -6,6 +6,7 @@ import { Metadata } from '@grpc/grpc-js'
import { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation' import { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation'
import { Status } from '@grpc/grpc-js/build/src/constants' import { Status } from '@grpc/grpc-js/build/src/constants'
import { Logger } from 'winston' import { Logger } from 'winston'
import { ResponseLocals } from '../../Controller/ResponseLocals'
export class GRPCSyncingServerServiceProxy { export class GRPCSyncingServerServiceProxy {
constructor( constructor(
@@ -20,24 +21,26 @@ export class GRPCSyncingServerServiceProxy {
response: Response, response: Response,
payload?: Record<string, unknown> | string, payload?: Record<string, unknown> | string,
): Promise<{ status: number; data: unknown }> { ): Promise<{ status: number; data: unknown }> {
const locals = response.locals as ResponseLocals
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const syncRequest = this.syncRequestGRPCMapper.toProjection(payload as Record<string, unknown>) const syncRequest = this.syncRequestGRPCMapper.toProjection(payload as Record<string, unknown>)
const metadata = new Metadata() 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-snjs-version', request.headers['x-snjs-version'] as string)
metadata.set('x-read-only-access', response.locals.readOnlyAccess ? 'true' : 'false') metadata.set('x-read-only-access', locals.readOnlyAccess ? 'true' : 'false')
if (response.locals.readOnlyAccess) { if (locals.readOnlyAccess) {
this.logger.debug('Syncing with read-only access', { this.logger.debug('Syncing with read-only access', {
codeTag: 'GRPCSyncingServerServiceProxy', codeTag: 'GRPCSyncingServerServiceProxy',
userId: response.locals.user.uuid, userId: locals.user.uuid,
}) })
} }
if (response.locals.session) { if (locals.session) {
metadata.set('x-session-uuid', response.locals.session.uuid) 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) => { this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
if (error) { if (error) {
@@ -52,7 +55,7 @@ export class GRPCSyncingServerServiceProxy {
if (error.code === Status.INTERNAL) { if (error.code === Status.INTERNAL) {
this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, { this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, {
codeTag: 'GRPCSyncingServerServiceProxy', 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)}`, { this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, {
codeTag: 'GRPCSyncingServerServiceProxy.catch', codeTag: 'GRPCSyncingServerServiceProxy.catch',
userId: response.locals.user.uuid, userId: locals.user.uuid,
}) })
} }
+35
View File
@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.177.19](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.177.18...@standardnotes/auth-server@1.177.19) (2024-01-17)
### Bug Fixes
* **auth:** add server daily email backup permission for all versions of core user role ([#1028](https://github.com/standardnotes/server/issues/1028)) ([460fdf9](https://github.com/standardnotes/server/commit/460fdf9eafe2db629637ba481f2b135ed21560b9))
## [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) ## [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 **Note:** Version bump only for package @standardnotes/auth-server
+3 -1
View File
@@ -35,6 +35,7 @@ import { AuthService } from '@standardnotes/grpc'
import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest' import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest'
import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken' import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security' import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
import { ResponseLocals } from '../src/Infra/InversifyExpressUtils/ResponseLocals'
const container = new ContainerConfigLoader() const container = new ContainerConfigLoader()
void container.load().then((container) => { void container.load().then((container) => {
@@ -59,12 +60,13 @@ void container.load().then((container) => {
server.setErrorConfig((app) => { server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => { app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
const locals = response.locals as ResponseLocals
logger.error(`${error.stack}`, { logger.error(`${error.stack}`, {
method: request.method, method: request.method,
url: request.url, url: request.url,
snjs: request.headers['x-snjs-version'], snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-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({ response.status(500).send({
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class EnableEmailBackupsForAll1705493201352 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Core User v1 Permissions
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
("bde42e26-628c-44e6-9d76-21b08954b0bf", "eb0575a2-6e26-49e3-9501-f2e75d7dbda3") \
',
)
// Core User v2 Permissions
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
("23bf88ca-bee1-4a4c-adf0-b7a48749eea7", "eb0575a2-6e26-49e3-9501-f2e75d7dbda3") \
',
)
}
public async down(): Promise<void> {
return
}
}
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class EnableEmailBackupsForAll1705493490376 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Core User v1 Permissions
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
("bde42e26-628c-44e6-9d76-21b08954b0bf", "eb0575a2-6e26-49e3-9501-f2e75d7dbda3") \
',
)
// Core User v2 Permissions
await queryRunner.query(
'INSERT INTO `role_permissions` (role_uuid, permission_uuid) VALUES \
("23bf88ca-bee1-4a4c-adf0-b7a48749eea7", "eb0575a2-6e26-49e3-9501-f2e75d7dbda3") \
',
)
}
public async down(): Promise<void> {
return
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/auth-server", "name": "@standardnotes/auth-server",
"version": "1.177.13", "version": "1.177.19",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+14 -2
View File
@@ -284,6 +284,7 @@ import { AccountDeletionVerificationPassedEventHandler } from '../Domain/Handler
import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions' import { RenewSharedSubscriptions } from '../Domain/UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser' import { FixStorageQuotaForUser } from '../Domain/UseCase/FixStorageQuotaForUser/FixStorageQuotaForUser'
import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler' import { FileQuotaRecalculatedEventHandler } from '../Domain/Handler/FileQuotaRecalculatedEventHandler'
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
export class ContainerConfigLoader { export class ContainerConfigLoader {
constructor(private mode: 'server' | 'worker' = 'server') {} constructor(private mode: 'server' | 'worker' = 'server') {}
@@ -986,7 +987,18 @@ export class ContainerConfigLoader {
.toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository))) .toConstantValue(new CleanupExpiredSessions(container.get(TYPES.Auth_SessionRepository)))
container.bind<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser) container.bind<AuthenticateUser>(TYPES.Auth_AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest).to(AuthenticateRequest) 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<SignIn>(TYPES.Auth_SignIn).to(SignIn)
container container
.bind<VerifyMFA>(TYPES.Auth_VerifyMFA) .bind<VerifyMFA>(TYPES.Auth_VerifyMFA)
@@ -1409,6 +1421,7 @@ export class ContainerConfigLoader {
container.get<SetSettingValue>(TYPES.Auth_SetSettingValue), container.get<SetSettingValue>(TYPES.Auth_SetSettingValue),
container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService), container.get<OfflineSettingServiceInterface>(TYPES.Auth_OfflineSettingService),
container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder), container.get<ContentDecoderInterface>(TYPES.Auth_ContenDecoder),
container.get<RenewSharedSubscriptions>(TYPES.Auth_RenewSharedSubscriptions),
container.get<winston.Logger>(TYPES.Auth_Logger), container.get<winston.Logger>(TYPES.Auth_Logger),
), ),
) )
@@ -1708,7 +1721,6 @@ export class ContainerConfigLoader {
.bind<BaseUsersController>(TYPES.Auth_BaseUsersController) .bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
.toConstantValue( .toConstantValue(
new BaseUsersController( new BaseUsersController(
container.get<UpdateUser>(TYPES.Auth_UpdateUser),
container.get<DeleteAccount>(TYPES.Auth_DeleteAccount), container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription), container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts), container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),
@@ -34,6 +34,19 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
event.payload.timestamp, 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) await this.roleService.setOfflineUserRole(offlineUserSubscription)
return return
@@ -16,6 +16,7 @@ import { OfflineSettingName } from '../Setting/OfflineSettingName'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType' import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings' import { ApplyDefaultSubscriptionSettings } from '../UseCase/ApplyDefaultSubscriptionSettings/ApplyDefaultSubscriptionSettings'
import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue' import { SetSettingValue } from '../UseCase/SetSettingValue/SetSettingValue'
import { RenewSharedSubscriptions } from '../UseCase/RenewSharedSubscriptions/RenewSharedSubscriptions'
export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface { export class SubscriptionSyncRequestedEventHandler implements DomainEventHandlerInterface {
constructor( constructor(
@@ -27,11 +28,20 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
private setSettingValue: SetSettingValue, private setSettingValue: SetSettingValue,
private offlineSettingService: OfflineSettingServiceInterface, private offlineSettingService: OfflineSettingServiceInterface,
private contentDecoder: ContentDecoderInterface, private contentDecoder: ContentDecoderInterface,
private renewSharedSubscriptions: RenewSharedSubscriptions,
private logger: Logger, private logger: Logger,
) {} ) {}
async handle(event: SubscriptionSyncRequestedEvent): Promise<void> { async handle(event: SubscriptionSyncRequestedEvent): Promise<void> {
this.logger.info('Subscription sync requested', {
subscriptionId: event.payload.subscriptionId,
})
if (event.payload.offline) { if (event.payload.offline) {
this.logger.info('Syncing offline subscription', {
subscriptionId: event.payload.subscriptionId,
})
const offlineUserSubscription = await this.createOrUpdateOfflineSubscription( const offlineUserSubscription = await this.createOrUpdateOfflineSubscription(
event.payload.subscriptionId, event.payload.subscriptionId,
event.payload.subscriptionName, event.payload.subscriptionName,
@@ -41,6 +51,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp, 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) await this.roleService.setOfflineUserRole(offlineUserSubscription)
const offlineFeaturesTokenDecoded = this.contentDecoder.decode( const offlineFeaturesTokenDecoded = this.contentDecoder.decode(
@@ -60,11 +83,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
value: offlineFeaturesTokenDecoded.extensionKey, value: offlineFeaturesTokenDecoded.extensionKey,
}) })
this.logger.info('Offline subscription synced', {
subscriptionId: event.payload.subscriptionId,
})
return return
} }
const usernameOrError = Username.create(event.payload.userEmail) const usernameOrError = Username.create(event.payload.userEmail)
if (usernameOrError.isFailed()) { if (usernameOrError.isFailed()) {
this.logger.warn(`Could not sync subscription: ${usernameOrError.getError()}`, {
subscriptionId: event.payload.subscriptionId,
})
return return
} }
const username = usernameOrError.getValue() const username = usernameOrError.getValue()
@@ -72,10 +103,18 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
const user = await this.userRepository.findOneByUsernameOrEmail(username) const user = await this.userRepository.findOneByUsernameOrEmail(username)
if (user === null) { 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 return
} }
this.logger.info('Syncing subscription', {
userId: user.uuid,
subscriptionId: event.payload.subscriptionId,
})
const userSubscription = await this.createOrUpdateSubscription( const userSubscription = await this.createOrUpdateSubscription(
event.payload.subscriptionId, event.payload.subscriptionId,
event.payload.subscriptionName, event.payload.subscriptionName,
@@ -85,6 +124,19 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
event.payload.timestamp, 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) await this.roleService.addUserRoleBasedOnSubscription(user, event.payload.subscriptionName)
const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({ const applyingSettingsResult = await this.applyDefaultSubscriptionSettings.execute({
@@ -107,6 +159,11 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
if (result.isFailed()) { if (result.isFailed()) {
this.logger.error(`Could not set extension key for user ${user.uuid}`) 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( private async createOrUpdateSubscription(
@@ -7,6 +7,10 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston' import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' 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', () => { describe('RefreshSessionToken', () => {
let sessionService: SessionServiceInterface let sessionService: SessionServiceInterface
@@ -14,16 +18,20 @@ describe('RefreshSessionToken', () => {
let domainEventFactory: DomainEventFactoryInterface let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface let domainEventPublisher: DomainEventPublisherInterface
let timer: TimerInterface let timer: TimerInterface
let getSetting: GetSetting
let logger: Logger let logger: Logger
const createUseCase = () => const createUseCase = () =>
new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, logger) new RefreshSessionToken(sessionService, domainEventFactory, domainEventPublisher, timer, getSetting, logger)
beforeEach(() => { beforeEach(() => {
session = {} as jest.Mocked<Session> session = {} as jest.Mocked<Session>
session.uuid = '1-2-3' session.uuid = '1-2-3'
session.refreshExpiration = new Date(123) 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 = {} as jest.Mocked<SessionServiceInterface>
sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true) sessionService.isRefreshTokenMatchingHashedSessionToken = jest.fn().mockReturnValue(true)
sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false }) sessionService.getSessionFromToken = jest.fn().mockReturnValue({ session, isEphemeral: false })
@@ -69,6 +77,35 @@ describe('RefreshSessionToken', () => {
expect(domainEventPublisher.publish).toHaveBeenCalled() 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 () => { it('should refresh a session token even if publishing domain event fails', async () => {
domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test')) domainEventPublisher.publish = jest.fn().mockRejectedValue(new Error('test'))
@@ -1,23 +1,24 @@
import { inject, injectable } from 'inversify'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time' import { TimerInterface } from '@standardnotes/time'
import { SettingName } from '@standardnotes/domain-core'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { Logger } from 'winston' import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { SessionServiceInterface } from '../Session/SessionServiceInterface' import { SessionServiceInterface } from '../Session/SessionServiceInterface'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface' import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse' import { RefreshSessionTokenResponse } from './RefreshSessionTokenResponse'
import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO' import { RefreshSessionTokenDTO } from './RefreshSessionTokenDTO'
import { GetSetting } from './GetSetting/GetSetting'
@injectable()
export class RefreshSessionToken { export class RefreshSessionToken {
constructor( constructor(
@inject(TYPES.Auth_SessionService) private sessionService: SessionServiceInterface, private sessionService: SessionServiceInterface,
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface, private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface, private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface, private timer: TimerInterface,
@inject(TYPES.Auth_Logger) private logger: Logger, private getSetting: GetSetting,
private logger: Logger,
) {} ) {}
async execute(dto: RefreshSessionTokenDTO): Promise<RefreshSessionTokenResponse> { 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 }) const sessionPayload = await this.sessionService.refreshTokens({ session, isEphemeral })
@@ -64,4 +67,19 @@ export class RefreshSessionToken {
userUuid: session.userUuid, 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, controller,
httpDelete, httpDelete,
httpGet, httpGet,
httpPatch,
httpPut, httpPut,
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
results, results,
} from 'inversify-express-utils' } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types' import TYPES from '../../Bootstrap/Types'
import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount' import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
import { UpdateUser } from '../../Domain/UseCase/UpdateUser'
import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/GetUserSubscription' import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts' import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts' import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
@@ -21,26 +19,13 @@ import { BaseUsersController } from './Base/BaseUsersController'
@controller('/users') @controller('/users')
export class AnnotatedUsersController extends BaseUsersController { export class AnnotatedUsersController extends BaseUsersController {
constructor( constructor(
@inject(TYPES.Auth_UpdateUser) override updateUser: UpdateUser,
@inject(TYPES.Auth_DeleteAccount) override doDeleteAccount: DeleteAccount, @inject(TYPES.Auth_DeleteAccount) override doDeleteAccount: DeleteAccount,
@inject(TYPES.Auth_GetUserSubscription) override doGetUserSubscription: GetUserSubscription, @inject(TYPES.Auth_GetUserSubscription) override doGetUserSubscription: GetUserSubscription,
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts, @inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts, @inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials, @inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
) { ) {
super( super(doDeleteAccount, doGetUserSubscription, clearLoginAttempts, increaseLoginAttempts, changeCredentialsUseCase)
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)
} }
@httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware) @httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
@@ -8,6 +8,7 @@ import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAtte
import { SignIn } from '../../../Domain/UseCase/SignIn' import { SignIn } from '../../../Domain/UseCase/SignIn'
import { VerifyMFA } from '../../../Domain/UseCase/VerifyMFA' import { VerifyMFA } from '../../../Domain/UseCase/VerifyMFA'
import { AuthController } from '../../../Controller/AuthController' import { AuthController } from '../../../Controller/AuthController'
import { ResponseLocals } from '../ResponseLocals'
import { BaseHttpController, results } from 'inversify-express-utils' import { BaseHttpController, results } from 'inversify-express-utils'
export class BaseAuthController extends BaseHttpController { export class BaseAuthController extends BaseHttpController {
@@ -37,9 +38,11 @@ export class BaseAuthController extends BaseHttpController {
} }
async params(request: Request, response: Response): Promise<results.JsonResult> { 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({ const result = await this.getUserKeyParams.execute({
email: response.locals.user.email, email: locals.user.email,
authenticated: true, authenticated: true,
}) })
@@ -145,6 +148,8 @@ export class BaseAuthController extends BaseHttpController {
} }
async pkceParams(request: Request, response: Response): Promise<results.JsonResult> { async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
if (!request.body.code_challenge) { if (!request.body.code_challenge) {
return this.json( 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({ const result = await this.getUserKeyParams.execute({
email: response.locals.user.email, email: locals.user.email,
authenticated: true, authenticated: true,
codeChallenge: request.body.code_challenge as string, 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> { async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authController.generateRecoveryCodes({ const result = await this.authController.generateRecoveryCodes({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
return this.json(result.data, result.status) 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> { async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
const locals = response.locals as ResponseLocals
const result = await this.authController.signOut({ const result = await this.authController.signOut({
readOnlyAccess: response.locals.readOnlyAccess, readOnlyAccess: locals.readOnlyAccess,
authorizationHeader: <string>request.headers.authorization, authorizationHeader: <string>request.headers.authorization,
}) })
@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { AuthenticatorsController } from '../../../Controller/AuthenticatorsController' import { AuthenticatorsController } from '../../../Controller/AuthenticatorsController'
import { BaseHttpController, results } from 'inversify-express-utils' import { BaseHttpController, results } from 'inversify-express-utils'
import { ResponseLocals } from '../ResponseLocals'
export class BaseAuthenticatorsController extends BaseHttpController { export class BaseAuthenticatorsController extends BaseHttpController {
constructor( constructor(
@@ -30,16 +31,20 @@ export class BaseAuthenticatorsController extends BaseHttpController {
} }
async list(_request: Request, response: Response): Promise<results.JsonResult> { async list(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.list({ const result = await this.authenticatorsController.list({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
return this.json(result.data, result.status) return this.json(result.data, result.status)
} }
async delete(request: Request, response: Response): Promise<results.JsonResult> { async delete(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.delete({ const result = await this.authenticatorsController.delete({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
authenticatorId: request.params.authenticatorId, authenticatorId: request.params.authenticatorId,
}) })
@@ -47,17 +52,21 @@ export class BaseAuthenticatorsController extends BaseHttpController {
} }
async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> { async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.generateRegistrationOptions({ const result = await this.authenticatorsController.generateRegistrationOptions({
username: response.locals.user.email, username: locals.user.email,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
return this.json(result.data, result.status) return this.json(result.data, result.status)
} }
async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> { async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.authenticatorsController.verifyRegistrationResponse({ const result = await this.authenticatorsController.verifyRegistrationResponse({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
attestationResponse: request.body.attestationResponse, attestationResponse: request.body.attestationResponse,
}) })
@@ -3,6 +3,7 @@ import { Request, Response } from 'express'
import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures' import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { BaseHttpController, results } from 'inversify-express-utils' import { BaseHttpController, results } from 'inversify-express-utils'
import { ResponseLocals } from '../ResponseLocals'
export class BaseFeaturesController extends BaseHttpController { export class BaseFeaturesController extends BaseHttpController {
constructor( constructor(
@@ -17,7 +18,9 @@ export class BaseFeaturesController extends BaseHttpController {
} }
async getFeatures(request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -4,6 +4,7 @@ import { Request, Response } from 'express'
import { CreateListedAccount } from '../../../Domain/UseCase/CreateListedAccount/CreateListedAccount' import { CreateListedAccount } from '../../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
import { BaseHttpController, results } from 'inversify-express-utils' import { BaseHttpController, results } from 'inversify-express-utils'
import { ResponseLocals } from '../ResponseLocals'
export class BaseListedController extends BaseHttpController { export class BaseListedController extends BaseHttpController {
constructor( constructor(
@@ -18,7 +19,9 @@ export class BaseListedController extends BaseHttpController {
} }
async createListedAccount(_request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -31,8 +34,8 @@ export class BaseListedController extends BaseHttpController {
} }
await this.doCreateListedAccount.execute({ await this.doCreateListedAccount.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
userEmail: response.locals.user.email, userEmail: locals.user.email,
}) })
return this.json({ return this.json({
@@ -8,6 +8,7 @@ import { AuthenticateOfflineSubscriptionToken } from '../../../Domain/UseCase/Au
import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken' import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures' import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
import { GetUserOfflineSubscription } from '../../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription' import { GetUserOfflineSubscription } from '../../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
import { OfflineResponseLocals } from '../OfflineResponseLocals'
export class BaseOfflineController extends BaseHttpController { export class BaseOfflineController extends BaseHttpController {
constructor( constructor(
@@ -30,8 +31,10 @@ export class BaseOfflineController extends BaseHttpController {
} }
async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> { async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as OfflineResponseLocals
const result = await this.doGetUserFeatures.execute({ const result = await this.doGetUserFeatures.execute({
email: response.locals.offlineUserEmail, email: locals.userEmail,
offline: true, offline: true,
}) })
@@ -115,8 +118,10 @@ export class BaseOfflineController extends BaseHttpController {
} }
async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> { async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as OfflineResponseLocals
const result = await this.getUserOfflineSubscription.execute({ const result = await this.getUserOfflineSubscription.execute({
userEmail: response.locals.userEmail, userEmail: locals.userEmail,
}) })
if (result.success) { if (result.success) {
@@ -6,6 +6,7 @@ import { ErrorTag } from '@standardnotes/responses'
import { DeleteOtherSessionsForUser } from '../../../Domain/UseCase/DeleteOtherSessionsForUser' import { DeleteOtherSessionsForUser } from '../../../Domain/UseCase/DeleteOtherSessionsForUser'
import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser' import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken' import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSessionController extends BaseHttpController { export class BaseSessionController extends BaseHttpController {
constructor( constructor(
@@ -24,7 +25,9 @@ export class BaseSessionController extends BaseHttpController {
} }
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> { 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( return this.json(
{ {
error: { error: {
@@ -36,7 +39,7 @@ export class BaseSessionController extends BaseHttpController {
) )
} }
if (!request.body.uuid) { if (!request.body.uuid || !locals.session) {
return this.json( return this.json(
{ {
error: { 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( return this.json(
{ {
error: { error: {
@@ -59,7 +62,7 @@ export class BaseSessionController extends BaseHttpController {
} }
const useCaseResponse = await this.deleteSessionForUser.execute({ const useCaseResponse = await this.deleteSessionForUser.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
sessionUuid: request.body.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) return this.statusCode(204)
} }
@@ -83,7 +86,9 @@ export class BaseSessionController extends BaseHttpController {
_request: Request, _request: Request,
response: Response, response: Response,
): Promise<results.JsonResult | results.StatusCodeResult> { ): Promise<results.JsonResult | results.StatusCodeResult> {
if (response.locals.readOnlyAccess) { const locals = response.locals as ResponseLocals
if (locals.readOnlyAccess) {
return this.json( return this.json(
{ {
error: { error: {
@@ -95,7 +100,7 @@ export class BaseSessionController extends BaseHttpController {
) )
} }
if (!response.locals.user) { if (!locals.user || !locals.session) {
return this.json( return this.json(
{ {
error: { error: {
@@ -107,12 +112,12 @@ export class BaseSessionController extends BaseHttpController {
} }
await this.deleteOtherSessionsForUser.execute({ await this.deleteOtherSessionsForUser.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
currentSessionUuid: response.locals.session.uuid, currentSessionUuid: locals.session.uuid,
markAsRevoked: true, markAsRevoked: true,
}) })
response.setHeader('x-invalidate-cache', response.locals.user.uuid) response.setHeader('x-invalidate-cache', locals.user.uuid)
return this.statusCode(204) return this.statusCode(204)
} }
@@ -9,6 +9,7 @@ import { Session } from '../../../Domain/Session/Session'
import { BaseHttpController, results } from 'inversify-express-utils' import { BaseHttpController, results } from 'inversify-express-utils'
import { User } from '../../../Domain/User/User' import { User } from '../../../Domain/User/User'
import { SessionProjector } from '../../../Projection/SessionProjector' import { SessionProjector } from '../../../Projection/SessionProjector'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSessionsController extends BaseHttpController { export class BaseSessionsController extends BaseHttpController {
constructor( constructor(
@@ -67,12 +68,14 @@ export class BaseSessionsController extends BaseHttpController {
} }
async getSessions(_request: Request, response: Response): Promise<results.JsonResult> { 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([]) return this.json([])
} }
const useCaseResponse = await this.getActiveSessionsForUser.execute({ const useCaseResponse = await this.getActiveSessionsForUser.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
return this.json( return this.json(
@@ -80,7 +83,7 @@ export class BaseSessionsController extends BaseHttpController {
this.sessionProjector.projectCustom( this.sessionProjector.projectCustom(
SessionProjector.CURRENT_SESSION_PROJECTION.toString(), SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
session, session,
response.locals.session, locals.session,
), ),
), ),
) )
@@ -13,6 +13,7 @@ import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation' import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation' import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation'
import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions' import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSettingsController extends BaseHttpController { export class BaseSettingsController extends BaseHttpController {
constructor( constructor(
@@ -40,7 +41,9 @@ export class BaseSettingsController extends BaseHttpController {
} }
async getSettings(request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -86,7 +89,9 @@ export class BaseSettingsController extends BaseHttpController {
} }
async getSetting(request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -135,7 +140,9 @@ export class BaseSettingsController extends BaseHttpController {
} }
async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> { 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( return this.json(
{ {
error: { 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( return this.json(
{ {
error: { error: {
@@ -163,7 +170,7 @@ export class BaseSettingsController extends BaseHttpController {
const result = await this.setSettingValue.execute({ const result = await this.setSettingValue.execute({
settingName: name, settingName: name,
value, value,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
checkUserPermissions: true, checkUserPermissions: true,
}) })
@@ -181,8 +188,8 @@ export class BaseSettingsController extends BaseHttpController {
const triggerResult = await this.triggerPostSettingUpdateActions.execute({ const triggerResult = await this.triggerPostSettingUpdateActions.execute({
updatedSettingName: setting.props.name, updatedSettingName: setting.props.name,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
userEmail: response.locals.user.email, userEmail: locals.user.email,
unencryptedValue: value, unencryptedValue: value,
}) })
if (triggerResult.isFailed()) { if (triggerResult.isFailed()) {
@@ -196,7 +203,9 @@ export class BaseSettingsController extends BaseHttpController {
} }
async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { 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( return this.json(
{ {
error: { error: {
@@ -4,7 +4,8 @@ import { BaseHttpController, results } from 'inversify-express-utils'
import { ApiVersion } from '@standardnotes/api' import { ApiVersion } from '@standardnotes/api'
import { SubscriptionInvitesController } from '../../../Controller/SubscriptionInvitesController' 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 { export class BaseSubscriptionInvitesController extends BaseHttpController {
constructor( constructor(
@@ -23,12 +24,14 @@ export class BaseSubscriptionInvitesController extends BaseHttpController {
} }
async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> { async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.acceptInvite({ const result = await this.subscriptionInvitesController.acceptInvite({
api: request.query.api as ApiVersion, api: request.query.api as ApiVersion,
inviteUuid: request.params.inviteUuid, 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) 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> { async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.invite({ const result = await this.subscriptionInvitesController.invite({
...request.body, ...request.body,
inviterEmail: response.locals.user.email, inviterEmail: locals.user.email,
inviterUuid: response.locals.user.uuid, inviterUuid: locals.user.uuid,
inviterRoles: response.locals.roles.map((role: Role) => role.name), inviterRoles: locals.roles.map((role: Role) => role.name),
}) })
return this.json(result.data, result.status) return this.json(result.data, result.status)
} }
async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> { async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.cancelInvite({ const result = await this.subscriptionInvitesController.cancelInvite({
...request.body, ...request.body,
inviteUuid: request.params.inviteUuid, inviteUuid: request.params.inviteUuid,
inviterEmail: response.locals.user.email, inviterEmail: locals.user.email,
}) })
return this.json(result.data, result.status) return this.json(result.data, result.status)
} }
async listInvites(request: Request, response: Response): Promise<results.JsonResult> { async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.subscriptionInvitesController.listInvites({ const result = await this.subscriptionInvitesController.listInvites({
...request.body, ...request.body,
inviterEmail: response.locals.user.email, inviterEmail: locals.user.email,
}) })
return this.json(result.data, result.status) 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 { GetSharedOrRegularSubscriptionForUser } from '../../../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser'
import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting' import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting'
import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation' import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSubscriptionSettingsController extends BaseHttpController { export class BaseSubscriptionSettingsController extends BaseHttpController {
constructor( constructor(
@@ -22,8 +23,10 @@ export class BaseSubscriptionSettingsController extends BaseHttpController {
} }
async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> { async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({ const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
if (subscriptionOrError.isFailed()) { if (subscriptionOrError.isFailed()) {
return this.json( return this.json(
@@ -9,6 +9,7 @@ import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscript
import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { User } from '../../../Domain/User/User' import { User } from '../../../Domain/User/User'
import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting' import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSubscriptionTokensController extends BaseHttpController { export class BaseSubscriptionTokensController extends BaseHttpController {
constructor( constructor(
@@ -29,7 +30,9 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
} }
async createToken(_request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -42,7 +45,7 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
} }
const result = await this.createSubscriptionToken.execute({ const result = await this.createSubscriptionToken.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
return this.json({ return this.json({
@@ -3,6 +3,7 @@ import { BaseHttpController, results } from 'inversify-express-utils'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { UserRequestsController } from '../../../Controller/UserRequestsController' import { UserRequestsController } from '../../../Controller/UserRequestsController'
import { ResponseLocals } from '../ResponseLocals'
export class BaseUserRequestsController extends BaseHttpController { export class BaseUserRequestsController extends BaseHttpController {
constructor( constructor(
@@ -17,10 +18,12 @@ export class BaseUserRequestsController extends BaseHttpController {
} }
async submitRequest(request: Request, response: Response): Promise<results.JsonResult> { async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.userRequestsController.submitUserRequest({ const result = await this.userRequestsController.submitUserRequest({
requestType: request.body.requestType, requestType: request.body.requestType,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
userEmail: response.locals.user.email, userEmail: locals.user.email,
}) })
return this.json(result.data, result.status) 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 { DeleteAccount } from '../../../Domain/UseCase/DeleteAccount/DeleteAccount'
import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription/GetUserSubscription' import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts' import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
import { ErrorTag } from '@standardnotes/responses' import { ErrorTag } from '@standardnotes/responses'
import { ResponseLocals } from '../ResponseLocals'
export class BaseUsersController extends BaseHttpController { export class BaseUsersController extends BaseHttpController {
constructor( constructor(
protected updateUser: UpdateUser,
protected doDeleteAccount: DeleteAccount, protected doDeleteAccount: DeleteAccount,
protected doGetUserSubscription: GetUserSubscription, protected doGetUserSubscription: GetUserSubscription,
protected clearLoginAttempts: ClearLoginAttempts, protected clearLoginAttempts: ClearLoginAttempts,
@@ -23,61 +22,16 @@ export class BaseUsersController extends BaseHttpController {
super() super()
if (this.controllerContainer !== undefined) { 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.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this)) this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
this.controllerContainer.register('auth.users.delete', this.deleteAccount.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> { 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( return this.json(
{ {
error: { error: {
@@ -107,7 +61,9 @@ export class BaseUsersController extends BaseHttpController {
} }
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -130,7 +86,9 @@ export class BaseUsersController extends BaseHttpController {
} }
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> { 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( return this.json(
{ {
error: { error: {
@@ -175,7 +133,7 @@ export class BaseUsersController extends BaseHttpController {
400, 400,
) )
} }
const usernameOrError = Username.create(response.locals.user.email) const usernameOrError = Username.create(locals.user.email)
if (usernameOrError.isFailed()) { if (usernameOrError.isFailed()) {
return this.json( return this.json(
{ {
@@ -202,7 +160,7 @@ export class BaseUsersController extends BaseHttpController {
}) })
if (changeCredentialsResult.isFailed()) { if (changeCredentialsResult.isFailed()) {
await this.increaseLoginAttempts.execute({ email: response.locals.user.email }) await this.increaseLoginAttempts.execute({ email: locals.user.email })
return this.json( 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()) return this.json(changeCredentialsResult.getValue())
} }
@@ -6,6 +6,7 @@ import { ValetTokenOperation } from '@standardnotes/security'
import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken' import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken'
import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload' import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload'
import { ResponseLocals } from '../ResponseLocals'
export class BaseValetTokenController extends BaseHttpController { export class BaseValetTokenController extends BaseHttpController {
constructor( constructor(
@@ -20,9 +21,11 @@ export class BaseValetTokenController extends BaseHttpController {
} }
public async create(request: Request, response: Response): Promise<results.JsonResult> { public async create(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const payload: CreateValetTokenPayload = request.body const payload: CreateValetTokenPayload = request.body
if (response.locals.readOnlyAccess && payload.operation !== 'read') { if (locals.readOnlyAccess && payload.operation !== 'read') {
return this.json( return this.json(
{ {
error: { error: {
@@ -50,7 +53,7 @@ export class BaseValetTokenController extends BaseHttpController {
} }
const createValetKeyResponse = await this.createValetKey.execute({ const createValetKeyResponse = await this.createValetKey.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
operation: payload.operation as ValetTokenOperation, operation: payload.operation as ValetTokenOperation,
resources: payload.resources, resources: payload.resources,
}) })
@@ -2,6 +2,7 @@ import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/sec
import { NextFunction, Request, Response } from 'express' import { NextFunction, Request, Response } from 'express'
import { BaseMiddleware } from 'inversify-express-utils' import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston' import { Logger } from 'winston'
import { ResponseLocals } from '../ResponseLocals'
export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware { export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
constructor( constructor(
@@ -34,10 +35,12 @@ export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware {
return return
} }
response.locals.user = token.user Object.assign(response.locals, {
response.locals.roles = token.roles user: token.user,
response.locals.session = token.session roles: token.roles,
response.locals.readOnlyAccess = token.session?.readonly_access ?? false session: token.session,
readOnlyAccess: token.session?.readonly_access ?? false,
} as ResponseLocals)
return next() return next()
} catch (error) { } catch (error) {
@@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify'
import { BaseMiddleware } from 'inversify-express-utils' import { BaseMiddleware } from 'inversify-express-utils'
import { Logger } from 'winston' import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
import { OfflineResponseLocals } from '../OfflineResponseLocals'
@injectable() @injectable()
export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware { export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware {
@@ -48,8 +49,10 @@ export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware {
return return
} }
response.locals.featuresToken = token.featuresToken Object.assign(response.locals, {
response.locals.userEmail = token.userEmail featuresToken: token.featuresToken,
userEmail: token.userEmail,
} as OfflineResponseLocals)
return next() return next()
} catch (error) { } catch (error) {
@@ -44,8 +44,8 @@ describe('OfflineUserAuthMiddleware', () => {
await createMiddleware().handler(request, response, next) await createMiddleware().handler(request, response, next)
expect(response.locals.offlineUserEmail).toEqual('test@test.com') expect(response.locals.userEmail).toEqual('test@test.com')
expect(response.locals.offlineFeaturesToken).toEqual('offline-features-token') expect(response.locals.featuresToken).toEqual('offline-features-token')
expect(next).toHaveBeenCalled() expect(next).toHaveBeenCalled()
}) })
@@ -5,6 +5,7 @@ import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types' import TYPES from '../../../Bootstrap/Types'
import { OfflineSettingName } from '../../../Domain/Setting/OfflineSettingName' import { OfflineSettingName } from '../../../Domain/Setting/OfflineSettingName'
import { OfflineSettingRepositoryInterface } from '../../../Domain/Setting/OfflineSettingRepositoryInterface' import { OfflineSettingRepositoryInterface } from '../../../Domain/Setting/OfflineSettingRepositoryInterface'
import { OfflineResponseLocals } from '../OfflineResponseLocals'
@injectable() @injectable()
export class OfflineUserAuthMiddleware extends BaseMiddleware { export class OfflineUserAuthMiddleware extends BaseMiddleware {
@@ -47,8 +48,10 @@ export class OfflineUserAuthMiddleware extends BaseMiddleware {
return return
} }
response.locals.offlineUserEmail = offlineFeaturesTokenSetting.email Object.assign(response.locals, {
response.locals.offlineFeaturesToken = offlineFeaturesTokenSetting.value featuresToken: offlineFeaturesTokenSetting.value,
userEmail: offlineFeaturesTokenSetting.email,
} as OfflineResponseLocals)
return next() return next()
} catch (error) { } 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, userUuid,
subscriptionId, subscriptionId,
}) })
.orderBy('ends_at', 'DESC')
.getOne() .getOne()
} }
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @standardnotes/files-server
+7 -3
View File
@@ -24,6 +24,10 @@ void container.load().then((container) => {
const env: Env = new Env() const env: Env = new Env()
env.load() 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) const server = new InversifyExpressServer(container)
server.setConfig((app) => { server.setConfig((app) => {
@@ -58,9 +62,9 @@ void container.load().then((container) => {
} }
})) }))
/* eslint-enable */ /* eslint-enable */
app.use(json({ limit: '50mb' })) app.use(json({ limit: requestPayloadLimit }))
app.use(raw({ limit: '50mb', type: 'application/octet-stream' })) app.use(raw({ limit: requestPayloadLimit, type: 'application/octet-stream' }))
app.use(urlencoded({ extended: true, limit: '50mb' })) app.use(urlencoded({ extended: true, limit: requestPayloadLimit }))
app.use( app.use(
cors({ cors({
exposedHeaders: ['Content-Range', 'Accept-Ranges'], exposedHeaders: ['Content-Range', 'Accept-Ranges'],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/files-server", "name": "@standardnotes/files-server",
"version": "1.37.8", "version": "1.37.9",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+42
View File
@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.60](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.59...@standardnotes/home-server@1.22.60) (2024-01-17)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.59](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.58...@standardnotes/home-server@1.22.59) (2024-01-17)
**Note:** Version bump only for package @standardnotes/home-server
## [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) ## [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 **Note:** Version bump only for package @standardnotes/home-server
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/home-server", "name": "@standardnotes/home-server",
"version": "1.22.50", "version": "1.22.60",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
+24 -4
View File
@@ -53,6 +53,10 @@ export class HomeServer implements HomeServerInterface {
const env: Env = new Env(environmentOverrides) const env: Env = new Env(environmentOverrides)
env.load() 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) this.configureLoggers(env, configuration)
const apiGatewayService = new ApiGatewayService(serviceContainer) const apiGatewayService = new ApiGatewayService(serviceContainer)
@@ -114,8 +118,8 @@ export class HomeServer implements HomeServerInterface {
} }
})) }))
/* eslint-enable */ /* eslint-enable */
app.use(json({ limit: '50mb' })) app.use(json({ limit: requestPayloadLimit }))
app.use(raw({ limit: '50mb', type: 'application/octet-stream' })) app.use(raw({ limit: requestPayloadLimit, type: 'application/octet-stream' }))
app.use( app.use(
text({ text({
type: [ type: [
@@ -160,8 +164,24 @@ export class HomeServer implements HomeServerInterface {
const logger: winston.Logger = winston.loggers.get('home-server') const logger: winston.Logger = winston.loggers.get('home-server')
server.setErrorConfig((app) => { server.setErrorConfig((app) => {
app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => { app.use((error: Record<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
logger.error(error.stack) logger.error(`${error.stack}`, {
method: request.method,
url: request.url,
snjs: request.headers['x-snjs-version'],
application: request.headers['x-application-version'],
userId: response.locals.user ? response.locals.user.uuid : undefined,
})
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({ response.status(500).send({
error: { error: {
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.135.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.134.1...@standardnotes/syncing-server@1.135.0) (2024-01-17)
### Features
* **syncing-server:** add procedure to recalculate content sizes ([#1027](https://github.com/standardnotes/server/issues/1027)) ([70bbf11](https://github.com/standardnotes/server/commit/70bbf11db504ed6305b9e9922bc38bd4b632b273))
## [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) ## [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 ### Bug Fixes
@@ -0,0 +1,50 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { FixContentSizes } from '../src/Domain/UseCase/Syncing/FixContentSizes/FixContentSizes'
import { Result } from '@standardnotes/domain-core'
const inputArgs = process.argv.slice(2)
const userUuid = inputArgs[0]
const container = new ContainerConfigLoader('worker')
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Sync_Logger)
logger.info('Starting fixing of content sizes', {
userId: userUuid,
})
const fixContentSizes = container.get<FixContentSizes>(TYPES.Sync_FixContentSizes)
Promise.resolve(fixContentSizes.execute({ userUuid }))
.then((result: Result<void>) => {
if (result.isFailed()) {
logger.error(`Error while fixing content sizes: ${result.getError()}`, {
userId: userUuid,
})
process.exit(1)
}
logger.info('Finished fixing of content sizes', {
userId: userUuid,
})
process.exit(0)
})
.catch((error) => {
logger.error(`Error while fixing content sizes: ${error.message}`, {
userId: userUuid,
})
process.exit(1)
})
})
+10 -4
View File
@@ -32,6 +32,10 @@ void container.load().then((container) => {
const env: Env = new Env() const env: Env = new Env()
env.load() 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) const server = new InversifyExpressServer(container)
server.setConfig((app) => { server.setConfig((app) => {
@@ -61,8 +65,8 @@ void container.load().then((container) => {
} }
})) }))
/* eslint-enable */ /* eslint-enable */
app.use(json({ limit: '50mb' })) app.use(json({ limit: requestPayloadLimit }))
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 5000 })) app.use(urlencoded({ extended: true, limit: requestPayloadLimit, parameterLimit: 5000 }))
app.use(cors()) app.use(cors())
}) })
@@ -115,8 +119,10 @@ void container.load().then((container) => {
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION), 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_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD), container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD), container.get<number>(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES), 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), container.get<winston.Logger>(TYPES.Sync_Logger),
) )
@@ -0,0 +1,11 @@
'use strict'
const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/content_size.js')))
Object.defineProperty(exports, '__esModule', { value: true })
exports.default = index
@@ -16,6 +16,11 @@ case "$COMMAND" in
exec node docker/entrypoint-statistics.js exec node docker/entrypoint-statistics.js
;; ;;
'content-size' )
EMAIL=$1 && shift 1
exec node docker/entrypoint-content-size.js $EMAIL
;;
* ) * )
echo "[Docker] Unknown command" echo "[Docker] Unknown command"
;; ;;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/syncing-server", "name": "@standardnotes/syncing-server",
"version": "1.133.4", "version": "1.135.0",
"engines": { "engines": {
"node": ">=18.0.0 <21.0.0" "node": ">=18.0.0 <21.0.0"
}, },
@@ -167,6 +167,7 @@ import { MetricsStoreInterface } from '../Domain/Metrics/MetricsStoreInterface'
import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore' import { RedisMetricStore } from '../Infra/Redis/RedisMetricStore'
import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore' import { DummyMetricStore } from '../Infra/Dummy/DummyMetricStore'
import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse' import { CheckForTrafficAbuse } from '../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
import { FixContentSizes } from '../Domain/UseCase/Syncing/FixContentSizes/FixContentSizes'
export class ContainerConfigLoader { export class ContainerConfigLoader {
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000 private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -479,7 +480,14 @@ export class ContainerConfigLoader {
container container
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD) .bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD)
.toConstantValue( .toConstantValue(
env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) ? +env.get('ITEM_OPERATIONS_ABUSE_THRESHOLD', true) : 500, 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 container
.bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES) .bind(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
@@ -489,15 +497,24 @@ export class ContainerConfigLoader {
: 5, : 5,
) )
container container
.bind(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD) .bind(TYPES.Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD)
.toConstantValue( .toConstantValue(
env.get('PAYLOAD_SIZE_ABUSE_THRESHOLD', true) ? +env.get('PAYLOAD_SIZE_ABUSE_THRESHOLD', true) : 20_000_000, env.get('UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
? +env.get('UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
: 100_000_000,
) )
container container
.bind(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES) .bind(TYPES.Sync_FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD)
.toConstantValue( .toConstantValue(
env.get('PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', true) env.get('FREE_USERS_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD', true)
? +env.get('PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', 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, : 5,
) )
container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET')) container.bind(TYPES.Sync_AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
@@ -939,6 +956,14 @@ export class ContainerConfigLoader {
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher), container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
), ),
) )
container
.bind<FixContentSizes>(TYPES.Sync_FixContentSizes)
.toConstantValue(
new FixContentSizes(
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
container.get<Logger>(TYPES.Sync_Logger),
),
)
// Services // Services
container container
@@ -1145,8 +1170,10 @@ export class ContainerConfigLoader {
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION), 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_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD), container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD), container.get<number>(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES), 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), container.get<ControllerContainerInterface>(TYPES.Sync_ControllerContainer),
), ),
) )
@@ -47,9 +47,11 @@ const TYPES = {
'Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', 'Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES',
), ),
Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD'), Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD: Symbol.for('Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD'), Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD: Symbol.for('Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD'),
Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES: Symbol.for( Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD: Symbol.for('Sync_UPLOAD_BANDWIDTH_ABUSE_THRESHOLD'),
'Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES', 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 // use cases
Sync_SyncItems: Symbol.for('Sync_SyncItems'), Sync_SyncItems: Symbol.for('Sync_SyncItems'),
@@ -95,6 +97,7 @@ const TYPES = {
Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'), Sync_TransferSharedVaultItems: Symbol.for('Sync_TransferSharedVaultItems'),
Sync_DumpItem: Symbol.for('Sync_DumpItem'), Sync_DumpItem: Symbol.for('Sync_DumpItem'),
Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'), Sync_CheckForTrafficAbuse: Symbol.for('Sync_CheckForTrafficAbuse'),
Sync_FixContentSizes: Symbol.for('Sync_FixContentSizes'),
// Handlers // Handlers
Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'), Sync_AccountDeletionRequestedEventHandler: Symbol.for('Sync_AccountDeletionRequestedEventHandler'),
Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for( Sync_AccountDeletionVerificationRequestedEventHandler: Symbol.for(
@@ -249,4 +249,25 @@ describe('Item', () => {
expect(entity.isIdenticalTo(otherEntity)).toBeFalsy() expect(entity.isIdenticalTo(otherEntity)).toBeFalsy()
}) })
it('should calculate content size of the item', () => {
const entity = Item.create(
{
duplicateOf: null,
itemsKeyId: 'items-key-id',
content: 'content',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: 'enc-item-key',
authHash: 'auth-hash',
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
deleted: false,
updatedWithSession: null,
dates: Dates.create(new Date(123), new Date(123)).getValue(),
timestamps: Timestamps.create(123, 123).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
expect(entity.calculateContentSize()).toEqual(943)
})
}) })
@@ -16,6 +16,10 @@ export class Item extends Aggregate<ItemProps> {
return Result.ok<Item>(new Item(props, id)) return Result.ok<Item>(new Item(props, id))
} }
calculateContentSize(): number {
return Buffer.byteLength(JSON.stringify(this))
}
get uuid(): Uuid { get uuid(): Uuid {
const uuidOrError = Uuid.create(this._id.toString()) const uuidOrError = Uuid.create(this._id.toString())
if (uuidOrError.isFailed()) { if (uuidOrError.isFailed()) {
@@ -0,0 +1,92 @@
import { Logger } from 'winston'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { FixContentSizes } from './FixContentSizes'
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
import { Item } from '../../../Item/Item'
describe('FixContentSizes', () => {
let itemRepository: ItemRepositoryInterface
let logger: Logger
const createUseCase = () => new FixContentSizes(itemRepository, logger)
beforeEach(() => {
const existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: false,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
itemRepository.findAll = jest.fn().mockReturnValue([existingItem])
itemRepository.countAll = jest.fn().mockReturnValue(1)
itemRepository.updateContentSize = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
})
it('should fix content sizes', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(itemRepository.updateContentSize).toHaveBeenCalledTimes(1)
expect(itemRepository.updateContentSize).toHaveBeenCalledWith('00000000-0000-0000-0000-000000000000', 947)
})
it('should return an error if user uuid is invalid', async () => {
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: 'invalid',
})
expect(result.isFailed()).toBeTruthy()
expect(result.getError()).toEqual('Given value is not a valid uuid: invalid')
})
it('should do nothing if the content size is correct', async () => {
const existingItem = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
contentSize: 947,
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: false,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
itemRepository.findAll = jest.fn().mockReturnValue([existingItem])
const useCase = createUseCase()
const result = await useCase.execute({
userUuid: '00000000-0000-0000-0000-000000000000',
})
expect(result.isFailed()).toBeFalsy()
expect(itemRepository.updateContentSize).toHaveBeenCalledTimes(0)
})
})
@@ -0,0 +1,65 @@
import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { ItemRepositoryInterface } from '../../../Item/ItemRepositoryInterface'
import { FixContentSizesDTO } from './FixContentSizesDTO'
export class FixContentSizes implements UseCaseInterface<void> {
constructor(
private itemRepository: ItemRepositoryInterface,
private logger: Logger,
) {}
async execute(dto: FixContentSizesDTO): Promise<Result<void>> {
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
}
const userUuid = userUuidOrError.getValue()
const count = await this.itemRepository.countAll({
userUuid: userUuid.value,
})
this.logger.info(`Fixing content sizes for ${count} items`, {
userId: userUuid.value,
codeTag: 'FixContentSizes',
})
const pageSize = 100
let page = 1
const totalPages = Math.ceil(count / pageSize)
for (page; page <= totalPages; page++) {
const items = await this.itemRepository.findAll({
userUuid: userUuid.value,
sortOrder: 'ASC',
sortBy: 'created_at_timestamp',
offset: (page - 1) * pageSize,
limit: pageSize,
})
for (const item of items) {
if (item.props.contentSize != item.calculateContentSize()) {
this.logger.info(`Fixing content size for item ${item.id}`, {
userId: userUuid.value,
codeTag: 'FixContentSizes',
itemUuid: item.uuid.value,
oldContentSize: item.props.contentSize,
newContentSize: item.calculateContentSize(),
})
await this.itemRepository.updateContentSize(item.id.toString(), item.calculateContentSize())
}
}
}
this.logger.info(`Finished fixing content sizes for ${count} items`, {
userId: userUuid.value,
codeTag: 'FixContentSizes',
})
return Result.ok()
}
}
@@ -0,0 +1,3 @@
export interface FixContentSizesDTO {
userUuid: string
}
@@ -29,8 +29,11 @@ export class AnnotatedItemsController extends BaseItemsController {
@inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES) @inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES)
override itemOperationsAbuseTimeframeLengthInMinutes: number, override itemOperationsAbuseTimeframeLengthInMinutes: number,
@inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD) override itemOperationsAbuseThreshold: number, @inject(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD) override itemOperationsAbuseThreshold: number,
@inject(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD) override payloadSizeAbuseThreshold: number, @inject(TYPES.Sync_FREE_USERS_ITEM_OPERATIONS_ABUSE_THRESHOLD)
@inject(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES) 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, override payloadSizeAbuseTimeframeLengthInMinutes: number,
) { ) {
super( super(
@@ -44,7 +47,9 @@ export class AnnotatedItemsController extends BaseItemsController {
strictAbuseProtection, strictAbuseProtection,
itemOperationsAbuseTimeframeLengthInMinutes, itemOperationsAbuseTimeframeLengthInMinutes,
itemOperationsAbuseThreshold, itemOperationsAbuseThreshold,
freeUsersItemOperationsAbuseThreshold,
payloadSizeAbuseThreshold, payloadSizeAbuseThreshold,
freeUsersPayloadSizeAbuseThreshold,
payloadSizeAbuseTimeframeLengthInMinutes, payloadSizeAbuseTimeframeLengthInMinutes,
) )
} }
@@ -14,6 +14,7 @@ import { ItemHash } from '../../../Domain/Item/ItemHash'
import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse' import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
import { Metric } from '../../../Domain/Metrics/Metric' import { Metric } from '../../../Domain/Metrics/Metric'
import { Logger } from 'winston' import { Logger } from 'winston'
import { ResponseLocals } from '../ResponseLocals'
export class BaseItemsController extends BaseHttpController { export class BaseItemsController extends BaseHttpController {
constructor( constructor(
@@ -27,7 +28,9 @@ export class BaseItemsController extends BaseHttpController {
protected strictAbuseProtection: boolean, protected strictAbuseProtection: boolean,
protected itemOperationsAbuseTimeframeLengthInMinutes: number, protected itemOperationsAbuseTimeframeLengthInMinutes: number,
protected itemOperationsAbuseThreshold: number, protected itemOperationsAbuseThreshold: number,
protected freeUsersItemOperationsAbuseThreshold: number,
protected payloadSizeAbuseThreshold: number, protected payloadSizeAbuseThreshold: number,
protected freeUsersPayloadSizeAbuseThreshold: number,
protected payloadSizeAbuseTimeframeLengthInMinutes: number, protected payloadSizeAbuseTimeframeLengthInMinutes: number,
private controllerContainer?: ControllerContainerInterface, private controllerContainer?: ControllerContainerInterface,
) { ) {
@@ -41,34 +44,51 @@ export class BaseItemsController extends BaseHttpController {
} }
async sync(request: Request, response: Response): Promise<results.JsonResult> { async sync(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({ const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ItemOperation, metricToCheck: Metric.NAMES.ItemOperation,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
threshold: this.itemOperationsAbuseThreshold, threshold: locals.isFreeUser ? this.freeUsersItemOperationsAbuseThreshold : this.itemOperationsAbuseThreshold,
timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes, timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
}) })
if (checkForItemOperationsAbuseResult.isFailed()) { if (checkForItemOperationsAbuseResult.isFailed()) {
this.logger.warn(checkForItemOperationsAbuseResult.getError(), { this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
userId: response.locals.user.uuid, userId: locals.user.uuid,
}) })
if (this.strictAbuseProtection) { if (this.strictAbuseProtection) {
return this.json({ error: { message: checkForItemOperationsAbuseResult.getError() } }, 429) 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({ const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ContentSizeUtilized, metricToCheck: Metric.NAMES.ContentSizeUtilized,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
threshold: this.payloadSizeAbuseThreshold, threshold: locals.isFreeUser ? this.freeUsersPayloadSizeAbuseThreshold : this.payloadSizeAbuseThreshold,
timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes, timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
}) })
if (checkForPayloadSizeAbuseResult.isFailed()) { if (checkForPayloadSizeAbuseResult.isFailed()) {
this.logger.warn(checkForPayloadSizeAbuseResult.getError(), { this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
userId: response.locals.user.uuid, userId: locals.user.uuid,
}) })
if (this.strictAbuseProtection) { if (this.strictAbuseProtection) {
return this.json({ error: { message: checkForPayloadSizeAbuseResult.getError() } }, 429) 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,
)
} }
} }
@@ -77,7 +97,7 @@ export class BaseItemsController extends BaseHttpController {
for (const itemHashInput of request.body.items) { for (const itemHashInput of request.body.items) {
const itemHashOrError = ItemHash.create({ const itemHashOrError = ItemHash.create({
...itemHashInput, ...itemHashInput,
user_uuid: response.locals.user.uuid, user_uuid: locals.user.uuid,
key_system_identifier: itemHashInput.key_system_identifier ?? null, key_system_identifier: itemHashInput.key_system_identifier ?? null,
shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null, shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null,
}) })
@@ -99,7 +119,7 @@ export class BaseItemsController extends BaseHttpController {
} }
const syncResult = await this.syncItems.execute({ const syncResult = await this.syncItems.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
itemHashes, itemHashes,
computeIntegrityHash: request.body.compute_integrity === true, computeIntegrityHash: request.body.compute_integrity === true,
syncToken: request.body.sync_token, syncToken: request.body.sync_token,
@@ -108,10 +128,10 @@ export class BaseItemsController extends BaseHttpController {
contentType: request.body.content_type, contentType: request.body.content_type,
apiVersion: request.body.api ?? ApiVersion.v20161215, apiVersion: request.body.api ?? ApiVersion.v20161215,
snjsVersion: <string>request.headers['x-snjs-version'], snjsVersion: <string>request.headers['x-snjs-version'],
readOnlyAccess: response.locals.readOnlyAccess, readOnlyAccess: locals.readOnlyAccess,
sessionUuid: response.locals.session ? response.locals.session.uuid : null, sessionUuid: locals.session ? locals.session.uuid : null,
sharedVaultUuids, sharedVaultUuids,
isFreeUser: response.locals.isFreeUser, isFreeUser: locals.isFreeUser,
}) })
if (syncResult.isFailed()) { if (syncResult.isFailed()) {
return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest) return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)
@@ -125,13 +145,15 @@ export class BaseItemsController extends BaseHttpController {
} }
async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> { async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
let integrityPayloads = [] let integrityPayloads = []
if ('integrityPayloads' in request.body) { if ('integrityPayloads' in request.body) {
integrityPayloads = request.body.integrityPayloads integrityPayloads = request.body.integrityPayloads
} }
const result = await this.checkIntegrity.execute({ const result = await this.checkIntegrity.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
integrityPayloads, integrityPayloads,
}) })
@@ -145,8 +167,10 @@ export class BaseItemsController extends BaseHttpController {
} }
async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> { async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getItem.execute({ const result = await this.getItem.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
itemUuid: request.params.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 { DeleteAllMessagesSentToUser } from '../../../Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser'
import { DeleteMessage } from '../../../Domain/UseCase/Messaging/DeleteMessage/DeleteMessage' import { DeleteMessage } from '../../../Domain/UseCase/Messaging/DeleteMessage/DeleteMessage'
import { GetMessagesSentByUser } from '../../../Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser' import { GetMessagesSentByUser } from '../../../Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser'
import { ResponseLocals } from '../ResponseLocals'
export class BaseMessagesController extends BaseHttpController { export class BaseMessagesController extends BaseHttpController {
constructor( constructor(
@@ -32,8 +33,10 @@ export class BaseMessagesController extends BaseHttpController {
} }
async getMessages(_request: Request, response: Response): Promise<results.JsonResult> { async getMessages(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getMessageSentToUserUseCase.execute({ const result = await this.getMessageSentToUserUseCase.execute({
recipientUuid: response.locals.user.uuid, recipientUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -53,8 +56,10 @@ export class BaseMessagesController extends BaseHttpController {
} }
async getMessagesSent(_request: Request, response: Response): Promise<results.JsonResult> { async getMessagesSent(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getMessagesSentByUserUseCase.execute({ const result = await this.getMessagesSentByUserUseCase.execute({
senderUuid: response.locals.user.uuid, senderUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -74,8 +79,10 @@ export class BaseMessagesController extends BaseHttpController {
} }
async sendMessage(request: Request, response: Response): Promise<results.JsonResult> { async sendMessage(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.sendMessageToUserUseCase.execute({ const result = await this.sendMessageToUserUseCase.execute({
senderUuid: response.locals.user.uuid, senderUuid: locals.user.uuid,
recipientUuid: request.body.recipient_uuid, recipientUuid: request.body.recipient_uuid,
encryptedMessage: request.body.encrypted_message, encryptedMessage: request.body.encrypted_message,
replaceabilityIdentifier: request.body.replaceability_identifier, replaceabilityIdentifier: request.body.replaceability_identifier,
@@ -98,8 +105,10 @@ export class BaseMessagesController extends BaseHttpController {
} }
async deleteMessagesSentToUser(_request: Request, response: Response): Promise<results.JsonResult> { async deleteMessagesSentToUser(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.deleteMessagesSentToUserUseCase.execute({ const result = await this.deleteMessagesSentToUserUseCase.execute({
recipientUuid: response.locals.user.uuid, recipientUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -117,9 +126,11 @@ export class BaseMessagesController extends BaseHttpController {
} }
async deleteMessage(request: Request, response: Response): Promise<results.JsonResult> { async deleteMessage(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.deleteMessageUseCase.execute({ const result = await this.deleteMessageUseCase.execute({
messageUuid: request.params.messageUuid, messageUuid: request.params.messageUuid,
originatorUuid: response.locals.user.uuid, originatorUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -13,6 +13,7 @@ import { DeleteSharedVaultInvitesToUser } from '../../../Domain/UseCase/SharedVa
import { GetSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser' import { GetSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser'
import { DeleteSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser' import { DeleteSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser'
import { GetSharedVaultInvitesSentToUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser' import { GetSharedVaultInvitesSentToUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSharedVaultInvitesController extends BaseHttpController { export class BaseSharedVaultInvitesController extends BaseHttpController {
constructor( constructor(
@@ -63,9 +64,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async createSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> { async createSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.inviteUserToSharedVaultUseCase.execute({ const result = await this.inviteUserToSharedVaultUseCase.execute({
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
senderUuid: response.locals.user.uuid, senderUuid: locals.user.uuid,
recipientUuid: request.body.recipient_uuid, recipientUuid: request.body.recipient_uuid,
encryptedMessage: request.body.encrypted_message, encryptedMessage: request.body.encrypted_message,
permission: request.body.permission, permission: request.body.permission,
@@ -88,10 +91,12 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async updateSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> { async updateSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.updateSharedVaultInviteUseCase.execute({ const result = await this.updateSharedVaultInviteUseCase.execute({
encryptedMessage: request.body.encrypted_message, encryptedMessage: request.body.encrypted_message,
inviteUuid: request.params.inviteUuid, inviteUuid: request.params.inviteUuid,
senderUuid: response.locals.user.uuid, senderUuid: locals.user.uuid,
permission: request.body.permission, permission: request.body.permission,
}) })
@@ -112,9 +117,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async acceptSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> { async acceptSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.acceptSharedVaultInviteUseCase.execute({ const result = await this.acceptSharedVaultInviteUseCase.execute({
inviteUuid: request.params.inviteUuid, inviteUuid: request.params.inviteUuid,
originatorUuid: response.locals.user.uuid, originatorUuid: locals.user.uuid,
}) })
if (result.isFailed()) { 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({ return this.json({
success: true, success: true,
@@ -136,9 +143,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> { async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.declineSharedVaultInviteUseCase.execute({ const result = await this.declineSharedVaultInviteUseCase.execute({
inviteUuid: request.params.inviteUuid, inviteUuid: request.params.inviteUuid,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -158,8 +167,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async deleteInboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> { async deleteInboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.deleteSharedVaultInvitesToUserUseCase.execute({ const result = await this.deleteSharedVaultInvitesToUserUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -179,8 +190,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async deleteOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> { async deleteOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({ const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -200,8 +213,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async getOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> { async getOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({ const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
senderUuid: response.locals.user.uuid, senderUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -221,8 +236,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async getSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> { async getSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({ const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
senderUuid: response.locals.user.uuid, senderUuid: locals.user.uuid,
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
}) })
@@ -243,8 +260,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async getUserInvites(_request: Request, response: Response): Promise<results.JsonResult> { async getUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getSharedVaultInvitesSentToUserUseCase.execute({ const result = await this.getSharedVaultInvitesSentToUserUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -264,9 +283,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> { async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.declineSharedVaultInviteUseCase.execute({ const result = await this.declineSharedVaultInviteUseCase.execute({
inviteUuid: request.params.inviteUuid, inviteUuid: request.params.inviteUuid,
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -286,8 +307,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
} }
async deleteAllSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> { async deleteAllSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({ const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
}) })
@@ -7,6 +7,7 @@ import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedV
import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers' import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers'
import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault' import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault'
import { DesignateSurvivor } from '../../../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor' import { DesignateSurvivor } from '../../../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSharedVaultUsersController extends BaseHttpController { export class BaseSharedVaultUsersController extends BaseHttpController {
constructor( constructor(
@@ -29,8 +30,10 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
} }
async getSharedVaultUsers(request: Request, response: Response): Promise<results.JsonResult> { async getSharedVaultUsers(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.getSharedVaultUsersUseCase.execute({ const result = await this.getSharedVaultUsersUseCase.execute({
originatorUuid: response.locals.user.uuid, originatorUuid: locals.user.uuid,
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
}) })
@@ -51,10 +54,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
} }
async removeUserFromSharedVault(request: Request, response: Response): Promise<results.JsonResult> { async removeUserFromSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.removeUserFromSharedVaultUseCase.execute({ const result = await this.removeUserFromSharedVaultUseCase.execute({
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
userUuid: request.params.userUuid, userUuid: request.params.userUuid,
originatorUuid: response.locals.user.uuid, originatorUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -76,10 +81,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
} }
async designateSurvivor(request: Request, response: Response): Promise<results.JsonResult> { async designateSurvivor(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.designateSurvivorUseCase.execute({ const result = await this.designateSurvivorUseCase.execute({
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
userUuid: request.params.userUuid, userUuid: request.params.userUuid,
originatorUuid: response.locals.user.uuid, originatorUuid: locals.user.uuid,
}) })
if (result.isFailed()) { if (result.isFailed()) {
@@ -11,6 +11,7 @@ import { CreateSharedVault } from '../../../Domain/UseCase/SharedVaults/CreateSh
import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation' import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation'
import { DeleteSharedVault } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault' import { DeleteSharedVault } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault'
import { CreateSharedVaultFileValetToken } from '../../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken' import { CreateSharedVaultFileValetToken } from '../../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken'
import { ResponseLocals } from '../ResponseLocals'
export class BaseSharedVaultsController extends BaseHttpController { export class BaseSharedVaultsController extends BaseHttpController {
constructor( constructor(
@@ -36,8 +37,10 @@ export class BaseSharedVaultsController extends BaseHttpController {
} }
async getSharedVaults(_request: Request, response: Response): Promise<results.JsonResult> { async getSharedVaults(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const resultOrError = await this.getSharedVaultsUseCase.execute({ const resultOrError = await this.getSharedVaultsUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
includeDesignatedSurvivors: true, includeDesignatedSurvivors: true,
}) })
@@ -64,9 +67,11 @@ export class BaseSharedVaultsController extends BaseHttpController {
} }
async createSharedVault(_request: Request, response: Response): Promise<results.JsonResult> { async createSharedVault(_request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.createSharedVaultUseCase.execute({ const result = await this.createSharedVaultUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
userRoleNames: response.locals.roles.map((role: Role) => role.name), userRoleNames: locals.roles.map((role: Role) => role.name),
}) })
if (result.isFailed()) { 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({ return this.json({
sharedVault: this.sharedVaultHttpMapper.toProjection(result.getValue().sharedVault), 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> { async deleteSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.deleteSharedVaultUseCase.execute({ const result = await this.deleteSharedVaultUseCase.execute({
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
originatorUuid: response.locals.user.uuid, originatorUuid: locals.user.uuid,
allowSurviving: false, 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 }) return this.json({ success: true })
} }
async createValetTokenForSharedVaultFile(request: Request, response: Response): Promise<results.JsonResult> { async createValetTokenForSharedVaultFile(request: Request, response: Response): Promise<results.JsonResult> {
const locals = response.locals as ResponseLocals
const result = await this.createSharedVaultFileValetTokenUseCase.execute({ const result = await this.createSharedVaultFileValetTokenUseCase.execute({
userUuid: response.locals.user.uuid, userUuid: locals.user.uuid,
sharedVaultUuid: request.params.sharedVaultUuid, sharedVaultUuid: request.params.sharedVaultUuid,
sharedVaultOwnerUploadBytesLimit: response.locals.sharedVaultOwnerContext?.upload_bytes_limit, sharedVaultOwnerUploadBytesLimit: locals.sharedVaultOwnerContext?.upload_bytes_limit,
fileUuid: request.body.file_uuid, fileUuid: request.body.file_uuid,
remoteIdentifier: request.body.remote_identifier, remoteIdentifier: request.body.remote_identifier,
operation: request.body.operation, operation: request.body.operation,
@@ -4,6 +4,7 @@ import { verify } from 'jsonwebtoken'
import { CrossServiceTokenData } from '@standardnotes/security' import { CrossServiceTokenData } from '@standardnotes/security'
import * as winston from 'winston' import * as winston from 'winston'
import { RoleName } from '@standardnotes/domain-core' import { RoleName } from '@standardnotes/domain-core'
import { ResponseLocals } from '../ResponseLocals'
export class InversifyExpressAuthMiddleware extends BaseMiddleware { export class InversifyExpressAuthMiddleware extends BaseMiddleware {
constructor( constructor(
@@ -25,13 +26,14 @@ export class InversifyExpressAuthMiddleware extends BaseMiddleware {
const decodedToken = <CrossServiceTokenData>verify(authToken, this.authJWTSecret, { algorithms: ['HS256'] }) const decodedToken = <CrossServiceTokenData>verify(authToken, this.authJWTSecret, { algorithms: ['HS256'] })
response.locals.user = decodedToken.user Object.assign(response.locals, {
response.locals.roles = decodedToken.roles user: decodedToken.user,
response.locals.isFreeUser = roles: decodedToken.roles,
decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
response.locals.session = decodedToken.session session: decodedToken.session,
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false readOnlyAccess: decodedToken.session?.readonly_access ?? false,
response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
} as ResponseLocals)
return next() return next()
} catch (error) { } 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
}
}
@@ -82,16 +82,16 @@ export class RedisMetricStore implements MetricsStoreInterface {
const date = this.timer.convertMicrosecondsToDate(metric.props.timestamp) const date = this.timer.convertMicrosecondsToDate(metric.props.timestamp)
const dateToTheMinuteString = this.timer.convertDateToFormattedString(date, 'YYYY-MM-DD HH:mm') 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 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() const pipeline = this.redisClient.pipeline()
pipeline.incrbyfloat(key, value) pipeline.incrbyfloat(key, value)
pipeline.incr( pipeline.incr(itemOperationKey)
`${this.METRIC_PER_USER_PREFIX}:${userUuid.value}:${Metric.NAMES.ItemOperation}:${dateToTheMinuteString}`,
)
const expirationTime = 60 * 60 * 24 const expirationTime = 60 * 60 * 24
pipeline.expire(key, expirationTime) pipeline.expire(key, expirationTime)
pipeline.expire(itemOperationKey, expirationTime)
await pipeline.exec() await pipeline.exec()
} }
@@ -21,7 +21,9 @@ export class SyncingServer implements ISyncingServer {
private strictAbuseProtection: boolean, private strictAbuseProtection: boolean,
private itemOperationsAbuseTimeframeLengthInMinutes: number, private itemOperationsAbuseTimeframeLengthInMinutes: number,
private itemOperationsAbuseThreshold: number, private itemOperationsAbuseThreshold: number,
private freeUsersItemOperationsAbuseThreshold: number,
private payloadSizeAbuseThreshold: number, private payloadSizeAbuseThreshold: number,
private freeUsersPayloadSizeAbuseThreshold: number,
private payloadSizeAbuseTimeframeLengthInMinutes: number, private payloadSizeAbuseTimeframeLengthInMinutes: number,
private logger: Logger, private logger: Logger,
) {} ) {}
@@ -32,11 +34,12 @@ export class SyncingServer implements ISyncingServer {
): Promise<void> { ): Promise<void> {
try { try {
const userUuid = call.metadata.get('x-user-uuid').pop() as string 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({ const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ItemOperation, metricToCheck: Metric.NAMES.ItemOperation,
userUuid, userUuid,
threshold: this.itemOperationsAbuseThreshold, threshold: isFreeUser ? this.freeUsersItemOperationsAbuseThreshold : this.itemOperationsAbuseThreshold,
timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes, timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
}) })
if (checkForItemOperationsAbuseResult.isFailed()) { if (checkForItemOperationsAbuseResult.isFailed()) {
@@ -45,7 +48,10 @@ export class SyncingServer implements ISyncingServer {
}) })
if (this.strictAbuseProtection) { if (this.strictAbuseProtection) {
const metadata = new grpc.Metadata() const metadata = new grpc.Metadata()
metadata.set('x-sync-error-message', checkForItemOperationsAbuseResult.getError()) 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') metadata.set('x-sync-error-response-code', '429')
return callback( return callback(
@@ -63,7 +69,7 @@ export class SyncingServer implements ISyncingServer {
const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({ const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ContentSizeUtilized, metricToCheck: Metric.NAMES.ContentSizeUtilized,
userUuid, userUuid,
threshold: this.payloadSizeAbuseThreshold, threshold: isFreeUser ? this.freeUsersPayloadSizeAbuseThreshold : this.payloadSizeAbuseThreshold,
timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes, timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
}) })
if (checkForPayloadSizeAbuseResult.isFailed()) { if (checkForPayloadSizeAbuseResult.isFailed()) {
@@ -73,7 +79,10 @@ export class SyncingServer implements ISyncingServer {
if (this.strictAbuseProtection) { if (this.strictAbuseProtection) {
const metadata = new grpc.Metadata() const metadata = new grpc.Metadata()
metadata.set('x-sync-error-message', checkForPayloadSizeAbuseResult.getError()) 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') metadata.set('x-sync-error-response-code', '429')
return callback( return callback(
@@ -158,7 +167,7 @@ export class SyncingServer implements ISyncingServer {
readOnlyAccess, readOnlyAccess,
sessionUuid: call.metadata.get('x-session-uuid').pop() as string, sessionUuid: call.metadata.get('x-session-uuid').pop() as string,
sharedVaultUuids, sharedVaultUuids,
isFreeUser: call.metadata.get('x-is-free-user').pop() === 'true', isFreeUser,
}) })
if (syncResult.isFailed()) { if (syncResult.isFailed()) {
const metadata = new grpc.Metadata() const metadata = new grpc.Metadata()