diff --git a/packages/api-gateway/bin/server.ts b/packages/api-gateway/bin/server.ts index f6b7d425b..c72a49730 100644 --- a/packages/api-gateway/bin/server.ts +++ b/packages/api-gateway/bin/server.ts @@ -36,6 +36,7 @@ import { InversifyExpressServer } from 'inversify-express-utils' import { ContainerConfigLoader } from '../src/Bootstrap/Container' import { TYPES } from '../src/Bootstrap/Types' import { Env } from '../src/Bootstrap/Env' +import { ResponseLocals } from '../src/Controller/ResponseLocals' const container = new ContainerConfigLoader() void container.load().then((container) => { @@ -91,12 +92,14 @@ void container.load().then((container) => { server.setErrorConfig((app) => { app.use((error: Record, request: Request, response: Response, _next: NextFunction) => { + const locals = response.locals as ResponseLocals + logger.error(`${error.stack}`, { method: request.method, url: request.url, snjs: request.headers['x-snjs-version'], application: request.headers['x-application-version'], - userId: response.locals.user ? response.locals.user.uuid : undefined, + userId: locals.user ? locals.user.uuid : undefined, }) logger.debug( `[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${ diff --git a/packages/api-gateway/src/Controller/AuthMiddleware.ts b/packages/api-gateway/src/Controller/AuthMiddleware.ts index f6bab3336..28f8dc08f 100644 --- a/packages/api-gateway/src/Controller/AuthMiddleware.ts +++ b/packages/api-gateway/src/Controller/AuthMiddleware.ts @@ -8,6 +8,8 @@ import { Logger } from 'winston' import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface' import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface' +import { ResponseLocals } from './ResponseLocals' +import { RoleName } from '@standardnotes/domain-core' export abstract class AuthMiddleware extends BaseMiddleware { constructor( @@ -55,33 +57,27 @@ export abstract class AuthMiddleware extends BaseMiddleware { crossServiceTokenFetchedFromCache = false } - response.locals.authToken = crossServiceToken - - const decodedToken = ( - verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] }) - ) + const decodedToken = verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] }) if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) { await this.crossServiceTokenCache.set({ key: cacheKey, - encodedCrossServiceToken: response.locals.authToken, + encodedCrossServiceToken: crossServiceToken, expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken), userUuid: decodedToken.user.uuid, }) } - response.locals.user = decodedToken.user - response.locals.session = decodedToken.session - response.locals.roles = decodedToken.roles - response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context - response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false - if (response.locals.readOnlyAccess) { - this.logger.debug('User operates on read-only access', { - codeTag: 'AuthMiddleware', - userId: response.locals.user.uuid, - }) - } - response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? [] + Object.assign(response.locals, { + authToken: crossServiceToken, + user: decodedToken.user, + session: decodedToken.session, + roles: decodedToken.roles, + sharedVaultOwnerContext: decodedToken.shared_vault_owner_context, + readOnlyAccess: decodedToken.session?.readonly_access ?? false, + isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser, + belongsToSharedVaults: decodedToken.belongs_to_shared_vaults ?? [], + } as ResponseLocals) } catch (error) { let detailedErrorMessage = (error as Error).message if (error instanceof AxiosError) { diff --git a/packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts b/packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts index 68582c7c1..d375fec30 100644 --- a/packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts +++ b/packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts @@ -6,6 +6,7 @@ import { verify } from 'jsonwebtoken' import { Logger } from 'winston' import { ConnectionValidationResponse, IAuthClient, WebsocketConnectionAuthorizationHeader } from '@standardnotes/grpc' import { RoleName } from '@standardnotes/domain-core' +import { ResponseLocals } from './ResponseLocals' export class GRPCWebSocketAuthMiddleware extends BaseMiddleware { constructor( @@ -90,15 +91,16 @@ export class GRPCWebSocketAuthMiddleware extends BaseMiddleware { const crossServiceToken = authResponse.data.authToken as string - response.locals.authToken = crossServiceToken - const decodedToken = verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] }) - response.locals.user = decodedToken.user - response.locals.session = decodedToken.session - response.locals.roles = decodedToken.roles - response.locals.isFreeUser = - decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser + Object.assign(response.locals, { + authToken: crossServiceToken, + user: decodedToken.user, + session: decodedToken.session, + roles: decodedToken.roles, + isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser, + readOnlyAccess: decodedToken.session?.readonly_access ?? false, + } as ResponseLocals) } catch (error) { this.logger.error( `Could not pass the request to websocket connection validation on underlying service: ${ diff --git a/packages/api-gateway/src/Controller/OfflineResponseLocals.ts b/packages/api-gateway/src/Controller/OfflineResponseLocals.ts new file mode 100644 index 000000000..68cd6fefb --- /dev/null +++ b/packages/api-gateway/src/Controller/OfflineResponseLocals.ts @@ -0,0 +1,5 @@ +export interface OfflineResponseLocals { + offlineAuthToken: string + userEmail: string + featuresToken: string +} diff --git a/packages/api-gateway/src/Controller/ResponseLocals.ts b/packages/api-gateway/src/Controller/ResponseLocals.ts new file mode 100644 index 000000000..903831864 --- /dev/null +++ b/packages/api-gateway/src/Controller/ResponseLocals.ts @@ -0,0 +1,29 @@ +import { Role } from '@standardnotes/security' + +export interface ResponseLocals { + authToken: string + user: { + uuid: string + email: string + } + roles: Array + 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 + } +} diff --git a/packages/api-gateway/src/Controller/SubscriptionResponseLocals.ts b/packages/api-gateway/src/Controller/SubscriptionResponseLocals.ts new file mode 100644 index 000000000..08dfa1e07 --- /dev/null +++ b/packages/api-gateway/src/Controller/SubscriptionResponseLocals.ts @@ -0,0 +1,5 @@ +import { TokenAuthenticationMethod } from './TokenAuthenticationMethod' + +export interface SubscriptionResponseLocals { + tokenAuthenticationMethod: TokenAuthenticationMethod +} diff --git a/packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts b/packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts index e7c82d94a..886d366dc 100644 --- a/packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts +++ b/packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts @@ -7,6 +7,9 @@ import { AxiosError, AxiosInstance, AxiosResponse } from 'axios' import { Logger } from 'winston' import { TYPES } from '../Bootstrap/Types' import { TokenAuthenticationMethod } from './TokenAuthenticationMethod' +import { ResponseLocals } from './ResponseLocals' +import { OfflineResponseLocals } from './OfflineResponseLocals' +import { SubscriptionResponseLocals } from './SubscriptionResponseLocals' @injectable() export class SubscriptionTokenAuthMiddleware extends BaseMiddleware { @@ -34,13 +37,16 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware { return } - response.locals.tokenAuthenticationMethod = email - ? TokenAuthenticationMethod.OfflineSubscriptionToken - : TokenAuthenticationMethod.SubscriptionToken + const locals = { + tokenAuthenticationMethod: email + ? TokenAuthenticationMethod.OfflineSubscriptionToken + : TokenAuthenticationMethod.SubscriptionToken, + } as SubscriptionResponseLocals + Object.assign(response.locals, locals) try { const url = - response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken + locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken ? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate` : `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate` @@ -65,7 +71,7 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware { return } - if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) { + if (locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) { this.handleOfflineAuthTokenValidationResponse(response, authResponse) return next() @@ -101,24 +107,26 @@ export class SubscriptionTokenAuthMiddleware extends BaseMiddleware { } private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) { - response.locals.offlineAuthToken = authResponse.data.authToken - const decodedToken = ( verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] }) ) - response.locals.offlineUserEmail = decodedToken.userEmail - response.locals.offlineFeaturesToken = decodedToken.featuresToken + Object.assign(response.locals, { + offlineAuthToken: authResponse.data.authToken, + userEmail: decodedToken.userEmail, + featuresToken: decodedToken.featuresToken, + } as OfflineResponseLocals) } private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) { - response.locals.authToken = authResponse.data.authToken - const decodedToken = ( verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] }) ) - response.locals.user = decodedToken.user - response.locals.roles = decodedToken.roles + Object.assign(response.locals, { + authToken: authResponse.data.authToken, + user: decodedToken.user, + roles: decodedToken.roles, + } as ResponseLocals) } } diff --git a/packages/api-gateway/src/Controller/WebSocketAuthMiddleware.ts b/packages/api-gateway/src/Controller/WebSocketAuthMiddleware.ts index 331730201..2e2f8d3a5 100644 --- a/packages/api-gateway/src/Controller/WebSocketAuthMiddleware.ts +++ b/packages/api-gateway/src/Controller/WebSocketAuthMiddleware.ts @@ -7,6 +7,7 @@ import { AxiosError, AxiosInstance } from 'axios' import { Logger } from 'winston' import { TYPES } from '../Bootstrap/Types' +import { ResponseLocals } from './ResponseLocals' @injectable() export class WebSocketAuthMiddleware extends BaseMiddleware { @@ -55,13 +56,14 @@ export class WebSocketAuthMiddleware extends BaseMiddleware { const crossServiceToken = authResponse.data.authToken - response.locals.authToken = crossServiceToken - const decodedToken = verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] }) - response.locals.user = decodedToken.user - response.locals.session = decodedToken.session - response.locals.roles = decodedToken.roles + Object.assign(response.locals, { + authToken: crossServiceToken, + user: decodedToken.user, + session: decodedToken.session, + roles: decodedToken.roles, + } as ResponseLocals) } catch (error) { const errorMessage = (error as AxiosError).isAxiosError ? JSON.stringify((error as AxiosError).response?.data) diff --git a/packages/api-gateway/src/Controller/v1/UsersController.ts b/packages/api-gateway/src/Controller/v1/UsersController.ts index d3f070bc2..0629cadfa 100644 --- a/packages/api-gateway/src/Controller/v1/UsersController.ts +++ b/packages/api-gateway/src/Controller/v1/UsersController.ts @@ -16,6 +16,8 @@ import { TYPES } from '../../Bootstrap/Types' import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface' import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod' import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface' +import { ResponseLocals } from '../ResponseLocals' +import { SubscriptionResponseLocals } from '../SubscriptionResponseLocals' @controller('/v1/users') export class UsersController extends BaseHttpController { @@ -214,7 +216,9 @@ export class UsersController extends BaseHttpController { @httpGet('/subscription', TYPES.ApiGateway_SubscriptionTokenAuthMiddleware) async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise { - if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) { + const locals = response.locals as SubscriptionResponseLocals & ResponseLocals + + if (locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) { await this.httpService.callAuthServer( request, response, @@ -227,11 +231,7 @@ export class UsersController extends BaseHttpController { await this.httpService.callAuthServer( request, response, - this.endpointResolver.resolveEndpointOrMethodIdentifier( - 'GET', - 'users/:userUuid/subscription', - response.locals.user.uuid, - ), + this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/:userUuid/subscription', locals.user.uuid), ) } diff --git a/packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts b/packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts index fa09d39a8..3d5565821 100644 --- a/packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts +++ b/packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express' import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core' import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface' +import { ResponseLocals } from '../../Controller/ResponseLocals' export class DirectCallServiceProxy implements ServiceProxyInterface { constructor( @@ -134,11 +135,13 @@ export class DirectCallServiceProxy implements ServiceProxyInterface { response: Response, serviceResponse: { statusCode: number; json: Record }, ): void { + const locals = response.locals as ResponseLocals + void response.status(serviceResponse.statusCode).send({ meta: { auth: { - userUuid: response.locals.user?.uuid, - roles: response.locals.roles, + userUuid: locals.user?.uuid, + roles: locals.roles, }, server: { filesServerUrl: this.filesServerUrl, diff --git a/packages/api-gateway/src/Service/Http/HttpServiceProxy.ts b/packages/api-gateway/src/Service/Http/HttpServiceProxy.ts index f16ed5f30..6285e90fc 100644 --- a/packages/api-gateway/src/Service/Http/HttpServiceProxy.ts +++ b/packages/api-gateway/src/Service/Http/HttpServiceProxy.ts @@ -8,6 +8,8 @@ import { TYPES } from '../../Bootstrap/Types' import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface' import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface' import { TimerInterface } from '@standardnotes/time' +import { ResponseLocals } from '../../Controller/ResponseLocals' +import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals' @injectable() export class HttpServiceProxy implements ServiceProxyInterface { @@ -176,6 +178,8 @@ export class HttpServiceProxy implements ServiceProxyInterface { endpoint: string, payload?: Record | string, ): Promise { + const locals = response.locals as ResponseLocals | OfflineResponseLocals + try { const headers: Record = {} for (const headerName of Object.keys(request.headers)) { @@ -185,12 +189,12 @@ export class HttpServiceProxy implements ServiceProxyInterface { delete headers.host delete headers['content-length'] - if (response.locals.authToken) { - headers['X-Auth-Token'] = response.locals.authToken + if ('authToken' in locals && locals.authToken) { + headers['X-Auth-Token'] = locals.authToken } - if (response.locals.offlineAuthToken) { - headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken + if ('offlineAuthToken' in locals && locals.offlineAuthToken) { + headers['X-Auth-Offline-Token'] = locals.offlineAuthToken } const serviceResponse = await this.httpClient.request({ @@ -222,7 +226,7 @@ export class HttpServiceProxy implements ServiceProxyInterface { this.logger.error( `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, payload?: Record | string, ): Promise { + const locals = response.locals as ResponseLocals const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload) if (!serviceResponse) { @@ -274,8 +279,8 @@ export class HttpServiceProxy implements ServiceProxyInterface { response.status(serviceResponse.status).send({ meta: { auth: { - userUuid: response.locals.user?.uuid, - roles: response.locals.roles, + userUuid: locals.user?.uuid, + roles: locals.roles, }, server: { filesServerUrl: this.filesServerUrl, diff --git a/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts b/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts index 38e43247c..3fcb6c1c6 100644 --- a/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts +++ b/packages/api-gateway/src/Service/Resolver/EndpointResolver.ts @@ -40,7 +40,6 @@ export class EndpointResolver implements EndpointResolverInterface { // Tokens Controller ['[POST]:subscription-tokens', 'auth.subscription-tokens.create'], // Users Controller - ['[PATCH]:users/:userId', 'auth.users.update'], ['[PUT]:users/:userUuid/attributes/credentials', 'auth.users.updateCredentials'], ['[DELETE]:users/:userUuid', 'auth.users.delete'], ['[POST]:listed', 'auth.users.createListedAccount'], diff --git a/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts b/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts index 6c6ce4eb9..9acf99463 100644 --- a/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts +++ b/packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts @@ -9,6 +9,8 @@ import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCache import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface' import { GRPCSyncingServerServiceProxy } from './GRPCSyncingServerServiceProxy' import { Status } from '@grpc/grpc-js/build/src/constants' +import { ResponseLocals } from '../../Controller/ResponseLocals' +import { OfflineResponseLocals } from '../../Controller/OfflineResponseLocals' export class GRPCServiceProxy implements ServiceProxyInterface { constructor( @@ -135,13 +137,15 @@ export class GRPCServiceProxy implements ServiceProxyInterface { response: Response, payload?: Record | string, ): Promise { + const locals = response.locals as ResponseLocals + const result = await this.gRPCSyncingServerServiceProxy.sync(request, response, payload) response.status(result.status).send({ meta: { auth: { - userUuid: response.locals.user?.uuid, - roles: response.locals.roles, + userUuid: locals.user?.uuid, + roles: locals.roles, }, server: { filesServerUrl: this.filesServerUrl, @@ -250,6 +254,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface { payload?: Record | string, retryAttempt?: number, ): Promise { + const locals = response.locals as ResponseLocals | OfflineResponseLocals + try { const headers: Record = {} for (const headerName of Object.keys(request.headers)) { @@ -259,12 +265,12 @@ export class GRPCServiceProxy implements ServiceProxyInterface { delete headers.host delete headers['content-length'] - if (response.locals.authToken) { - headers['X-Auth-Token'] = response.locals.authToken + if ('authToken' in locals && locals.authToken) { + headers['X-Auth-Token'] = locals.authToken } - if (response.locals.offlineAuthToken) { - headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken + if ('offlineAuthToken' in locals && locals.offlineAuthToken) { + headers['X-Auth-Offline-Token'] = locals.offlineAuthToken } const serviceResponse = await this.httpClient.request({ @@ -314,7 +320,7 @@ export class GRPCServiceProxy implements ServiceProxyInterface { ? `Request to ${serverUrl}/${endpoint} timed out after ${retryAttempt} retries` : `Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${detailedErrorMessage}`, { - userId: response.locals.user ? response.locals.user.uuid : undefined, + userId: (locals as ResponseLocals).user ? (locals as ResponseLocals).user.uuid : undefined, }, ) @@ -349,6 +355,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface { endpoint: string, payload?: Record | string, ): Promise { + const locals = response.locals as ResponseLocals + const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload) if (!serviceResponse) { @@ -366,8 +374,8 @@ export class GRPCServiceProxy implements ServiceProxyInterface { response.status(serviceResponse.status).send({ meta: { auth: { - userUuid: response.locals.user?.uuid, - roles: response.locals.roles, + userUuid: locals.user?.uuid, + roles: locals.roles, }, server: { filesServerUrl: this.filesServerUrl, diff --git a/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts b/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts index f76080cfc..a812617f8 100644 --- a/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts +++ b/packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts @@ -6,6 +6,7 @@ import { Metadata } from '@grpc/grpc-js' import { SyncResponseHttpRepresentation } from '../../Mapping/Sync/Http/SyncResponseHttpRepresentation' import { Status } from '@grpc/grpc-js/build/src/constants' import { Logger } from 'winston' +import { ResponseLocals } from '../../Controller/ResponseLocals' export class GRPCSyncingServerServiceProxy { constructor( @@ -20,24 +21,26 @@ export class GRPCSyncingServerServiceProxy { response: Response, payload?: Record | string, ): Promise<{ status: number; data: unknown }> { + const locals = response.locals as ResponseLocals + return new Promise((resolve, reject) => { try { const syncRequest = this.syncRequestGRPCMapper.toProjection(payload as Record) const metadata = new Metadata() - metadata.set('x-user-uuid', response.locals.user.uuid) + metadata.set('x-user-uuid', locals.user.uuid) metadata.set('x-snjs-version', request.headers['x-snjs-version'] as string) - metadata.set('x-read-only-access', response.locals.readOnlyAccess ? 'true' : 'false') - if (response.locals.readOnlyAccess) { + metadata.set('x-read-only-access', locals.readOnlyAccess ? 'true' : 'false') + if (locals.readOnlyAccess) { this.logger.debug('Syncing with read-only access', { codeTag: 'GRPCSyncingServerServiceProxy', - userId: response.locals.user.uuid, + userId: locals.user.uuid, }) } - if (response.locals.session) { - metadata.set('x-session-uuid', response.locals.session.uuid) + if (locals.session) { + metadata.set('x-session-uuid', locals.session.uuid) } - metadata.set('x-is-free-user', response.locals.isFreeUser ? 'true' : 'false') + metadata.set('x-is-free-user', locals.isFreeUser ? 'true' : 'false') this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => { if (error) { @@ -52,7 +55,7 @@ export class GRPCSyncingServerServiceProxy { if (error.code === Status.INTERNAL) { this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, { codeTag: 'GRPCSyncingServerServiceProxy', - userId: response.locals.user.uuid, + userId: locals.user.uuid, }) } @@ -68,7 +71,7 @@ export class GRPCSyncingServerServiceProxy { ) { this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, { codeTag: 'GRPCSyncingServerServiceProxy.catch', - userId: response.locals.user.uuid, + userId: locals.user.uuid, }) } diff --git a/packages/auth/bin/server.ts b/packages/auth/bin/server.ts index 0f48bd374..85f380f0f 100644 --- a/packages/auth/bin/server.ts +++ b/packages/auth/bin/server.ts @@ -35,6 +35,7 @@ import { AuthService } from '@standardnotes/grpc' import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest' import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken' import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security' +import { ResponseLocals } from '../src/Infra/InversifyExpressUtils/ResponseLocals' const container = new ContainerConfigLoader() void container.load().then((container) => { @@ -59,12 +60,13 @@ void container.load().then((container) => { server.setErrorConfig((app) => { app.use((error: Record, request: Request, response: Response, _next: NextFunction) => { + const locals = response.locals as ResponseLocals logger.error(`${error.stack}`, { method: request.method, url: request.url, snjs: request.headers['x-snjs-version'], application: request.headers['x-application-version'], - userId: response.locals.user ? response.locals.user.uuid : undefined, + userId: locals.user ? locals.user.uuid : undefined, }) response.status(500).send({ diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index 7110ce145..23fd0bcf7 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -1708,7 +1708,6 @@ export class ContainerConfigLoader { .bind(TYPES.Auth_BaseUsersController) .toConstantValue( new BaseUsersController( - container.get(TYPES.Auth_UpdateUser), container.get(TYPES.Auth_DeleteAccount), container.get(TYPES.Auth_GetUserSubscription), container.get(TYPES.Auth_ClearLoginAttempts), diff --git a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.ts b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.ts index 5bc74292d..07f1f0996 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.ts @@ -4,14 +4,12 @@ import { controller, httpDelete, httpGet, - httpPatch, httpPut, // eslint-disable-next-line @typescript-eslint/no-unused-vars results, } from 'inversify-express-utils' import TYPES from '../../Bootstrap/Types' import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount' -import { UpdateUser } from '../../Domain/UseCase/UpdateUser' import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/GetUserSubscription' import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts' import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts' @@ -21,26 +19,13 @@ import { BaseUsersController } from './Base/BaseUsersController' @controller('/users') export class AnnotatedUsersController extends BaseUsersController { constructor( - @inject(TYPES.Auth_UpdateUser) override updateUser: UpdateUser, @inject(TYPES.Auth_DeleteAccount) override doDeleteAccount: DeleteAccount, @inject(TYPES.Auth_GetUserSubscription) override doGetUserSubscription: GetUserSubscription, @inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts, @inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts, @inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials, ) { - super( - updateUser, - doDeleteAccount, - doGetUserSubscription, - clearLoginAttempts, - increaseLoginAttempts, - changeCredentialsUseCase, - ) - } - - @httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware) - override async update(request: Request, response: Response): Promise { - return super.update(request, response) + super(doDeleteAccount, doGetUserSubscription, clearLoginAttempts, increaseLoginAttempts, changeCredentialsUseCase) } @httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthController.ts index 2702240d5..9acb04625 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthController.ts @@ -8,6 +8,7 @@ import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAtte import { SignIn } from '../../../Domain/UseCase/SignIn' import { VerifyMFA } from '../../../Domain/UseCase/VerifyMFA' import { AuthController } from '../../../Controller/AuthController' +import { ResponseLocals } from '../ResponseLocals' import { BaseHttpController, results } from 'inversify-express-utils' export class BaseAuthController extends BaseHttpController { @@ -37,9 +38,11 @@ export class BaseAuthController extends BaseHttpController { } async params(request: Request, response: Response): Promise { - if (response.locals.session) { + const locals = response.locals as ResponseLocals + + if (locals.session) { const result = await this.getUserKeyParams.execute({ - email: response.locals.user.email, + email: locals.user.email, authenticated: true, }) @@ -145,6 +148,8 @@ export class BaseAuthController extends BaseHttpController { } async pkceParams(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + if (!request.body.code_challenge) { return this.json( { @@ -156,9 +161,9 @@ export class BaseAuthController extends BaseHttpController { ) } - if (response.locals.session) { + if (locals.session) { const result = await this.getUserKeyParams.execute({ - email: response.locals.user.email, + email: locals.user.email, authenticated: true, codeChallenge: request.body.code_challenge as string, }) @@ -248,8 +253,10 @@ export class BaseAuthController extends BaseHttpController { } async generateRecoveryCodes(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.authController.generateRecoveryCodes({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) return this.json(result.data, result.status) @@ -280,8 +287,10 @@ export class BaseAuthController extends BaseHttpController { } async signOut(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.authController.signOut({ - readOnlyAccess: response.locals.readOnlyAccess, + readOnlyAccess: locals.readOnlyAccess, authorizationHeader: request.headers.authorization, }) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController.ts index 1cc99d959..671bbc309 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController.ts @@ -3,6 +3,7 @@ import { Request, Response } from 'express' import { AuthenticatorsController } from '../../../Controller/AuthenticatorsController' import { BaseHttpController, results } from 'inversify-express-utils' +import { ResponseLocals } from '../ResponseLocals' export class BaseAuthenticatorsController extends BaseHttpController { constructor( @@ -30,16 +31,20 @@ export class BaseAuthenticatorsController extends BaseHttpController { } async list(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.authenticatorsController.list({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) return this.json(result.data, result.status) } async delete(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.authenticatorsController.delete({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, authenticatorId: request.params.authenticatorId, }) @@ -47,17 +52,21 @@ export class BaseAuthenticatorsController extends BaseHttpController { } async generateRegistrationOptions(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.authenticatorsController.generateRegistrationOptions({ - username: response.locals.user.email, - userUuid: response.locals.user.uuid, + username: locals.user.email, + userUuid: locals.user.uuid, }) return this.json(result.data, result.status) } async verifyRegistration(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.authenticatorsController.verifyRegistrationResponse({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, attestationResponse: request.body.attestationResponse, }) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseFeaturesController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseFeaturesController.ts index cbfaf38ab..a5b76e6df 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseFeaturesController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseFeaturesController.ts @@ -3,6 +3,7 @@ import { Request, Response } from 'express' import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures' import { BaseHttpController, results } from 'inversify-express-utils' +import { ResponseLocals } from '../ResponseLocals' export class BaseFeaturesController extends BaseHttpController { constructor( @@ -17,7 +18,9 @@ export class BaseFeaturesController extends BaseHttpController { } async getFeatures(request: Request, response: Response): Promise { - if (request.params.userUuid !== response.locals.user.uuid) { + const locals = response.locals as ResponseLocals + + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseListedController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseListedController.ts index 79e7699e8..820c88072 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseListedController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseListedController.ts @@ -4,6 +4,7 @@ import { Request, Response } from 'express' import { CreateListedAccount } from '../../../Domain/UseCase/CreateListedAccount/CreateListedAccount' import { BaseHttpController, results } from 'inversify-express-utils' +import { ResponseLocals } from '../ResponseLocals' export class BaseListedController extends BaseHttpController { constructor( @@ -18,7 +19,9 @@ export class BaseListedController extends BaseHttpController { } async createListedAccount(_request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -31,8 +34,8 @@ export class BaseListedController extends BaseHttpController { } await this.doCreateListedAccount.execute({ - userUuid: response.locals.user.uuid, - userEmail: response.locals.user.email, + userUuid: locals.user.uuid, + userEmail: locals.user.email, }) return this.json({ diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseOfflineController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseOfflineController.ts index eb383a59c..3dd33a865 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseOfflineController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseOfflineController.ts @@ -8,6 +8,7 @@ import { AuthenticateOfflineSubscriptionToken } from '../../../Domain/UseCase/Au import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken' import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures' import { GetUserOfflineSubscription } from '../../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription' +import { OfflineResponseLocals } from '../OfflineResponseLocals' export class BaseOfflineController extends BaseHttpController { constructor( @@ -30,8 +31,10 @@ export class BaseOfflineController extends BaseHttpController { } async getOfflineFeatures(_request: Request, response: Response): Promise { + const locals = response.locals as OfflineResponseLocals + const result = await this.doGetUserFeatures.execute({ - email: response.locals.offlineUserEmail, + email: locals.userEmail, offline: true, }) @@ -115,8 +118,10 @@ export class BaseOfflineController extends BaseHttpController { } async getSubscription(_request: Request, response: Response): Promise { + const locals = response.locals as OfflineResponseLocals + const result = await this.getUserOfflineSubscription.execute({ - userEmail: response.locals.userEmail, + userEmail: locals.userEmail, }) if (result.success) { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionController.ts index 571b73524..8e6243ad9 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionController.ts @@ -6,6 +6,7 @@ import { ErrorTag } from '@standardnotes/responses' import { DeleteOtherSessionsForUser } from '../../../Domain/UseCase/DeleteOtherSessionsForUser' import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser' import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken' +import { ResponseLocals } from '../ResponseLocals' export class BaseSessionController extends BaseHttpController { constructor( @@ -24,7 +25,9 @@ export class BaseSessionController extends BaseHttpController { } async deleteSession(request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -36,7 +39,7 @@ export class BaseSessionController extends BaseHttpController { ) } - if (!request.body.uuid) { + if (!request.body.uuid || !locals.session) { return this.json( { error: { @@ -47,7 +50,7 @@ export class BaseSessionController extends BaseHttpController { ) } - if (request.body.uuid === response.locals.session.uuid) { + if (request.body.uuid === locals.session.uuid) { return this.json( { error: { @@ -59,7 +62,7 @@ export class BaseSessionController extends BaseHttpController { } const useCaseResponse = await this.deleteSessionForUser.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, sessionUuid: request.body.uuid, }) @@ -74,7 +77,7 @@ export class BaseSessionController extends BaseHttpController { ) } - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.statusCode(204) } @@ -83,7 +86,9 @@ export class BaseSessionController extends BaseHttpController { _request: Request, response: Response, ): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -95,7 +100,7 @@ export class BaseSessionController extends BaseHttpController { ) } - if (!response.locals.user) { + if (!locals.user || !locals.session) { return this.json( { error: { @@ -107,12 +112,12 @@ export class BaseSessionController extends BaseHttpController { } await this.deleteOtherSessionsForUser.execute({ - userUuid: response.locals.user.uuid, - currentSessionUuid: response.locals.session.uuid, + userUuid: locals.user.uuid, + currentSessionUuid: locals.session.uuid, markAsRevoked: true, }) - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.statusCode(204) } diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionsController.ts index 82d2e22df..89d69f6f8 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionsController.ts @@ -9,6 +9,7 @@ import { Session } from '../../../Domain/Session/Session' import { BaseHttpController, results } from 'inversify-express-utils' import { User } from '../../../Domain/User/User' import { SessionProjector } from '../../../Projection/SessionProjector' +import { ResponseLocals } from '../ResponseLocals' export class BaseSessionsController extends BaseHttpController { constructor( @@ -67,12 +68,14 @@ export class BaseSessionsController extends BaseHttpController { } async getSessions(_request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json([]) } const useCaseResponse = await this.getActiveSessionsForUser.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) return this.json( @@ -80,7 +83,7 @@ export class BaseSessionsController extends BaseHttpController { this.sessionProjector.projectCustom( SessionProjector.CURRENT_SESSION_PROJECTION.toString(), session, - response.locals.session, + locals.session, ), ), ) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts index 058f82d8c..c797d85cb 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts @@ -13,6 +13,7 @@ import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation' import { SettingHttpRepresentation } from '../../../Mapping/Http/SettingHttpRepresentation' import { TriggerPostSettingUpdateActions } from '../../../Domain/UseCase/TriggerPostSettingUpdateActions/TriggerPostSettingUpdateActions' +import { ResponseLocals } from '../ResponseLocals' export class BaseSettingsController extends BaseHttpController { constructor( @@ -40,7 +41,9 @@ export class BaseSettingsController extends BaseHttpController { } async getSettings(request: Request, response: Response): Promise { - if (request.params.userUuid !== response.locals.user.uuid) { + const locals = response.locals as ResponseLocals + + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { @@ -86,7 +89,9 @@ export class BaseSettingsController extends BaseHttpController { } async getSetting(request: Request, response: Response): Promise { - if (request.params.userUuid !== response.locals.user.uuid) { + const locals = response.locals as ResponseLocals + + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { @@ -135,7 +140,9 @@ export class BaseSettingsController extends BaseHttpController { } async updateSetting(request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -147,7 +154,7 @@ export class BaseSettingsController extends BaseHttpController { ) } - if (request.params.userUuid !== response.locals.user.uuid) { + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { @@ -163,7 +170,7 @@ export class BaseSettingsController extends BaseHttpController { const result = await this.setSettingValue.execute({ settingName: name, value, - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, checkUserPermissions: true, }) @@ -181,8 +188,8 @@ export class BaseSettingsController extends BaseHttpController { const triggerResult = await this.triggerPostSettingUpdateActions.execute({ updatedSettingName: setting.props.name, - userUuid: response.locals.user.uuid, - userEmail: response.locals.user.email, + userUuid: locals.user.uuid, + userEmail: locals.user.email, unencryptedValue: value, }) if (triggerResult.isFailed()) { @@ -196,7 +203,9 @@ export class BaseSettingsController extends BaseHttpController { } async deleteSetting(request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -208,7 +217,7 @@ export class BaseSettingsController extends BaseHttpController { ) } - if (request.params.userUuid !== response.locals.user.uuid) { + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController.ts index 5a889ad09..41f066a32 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController.ts @@ -4,7 +4,8 @@ import { BaseHttpController, results } from 'inversify-express-utils' import { ApiVersion } from '@standardnotes/api' import { SubscriptionInvitesController } from '../../../Controller/SubscriptionInvitesController' -import { Role } from '../../../Domain/Role/Role' +import { ResponseLocals } from '../ResponseLocals' +import { Role } from '@standardnotes/security' export class BaseSubscriptionInvitesController extends BaseHttpController { constructor( @@ -23,12 +24,14 @@ export class BaseSubscriptionInvitesController extends BaseHttpController { } async acceptInvite(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.subscriptionInvitesController.acceptInvite({ api: request.query.api as ApiVersion, inviteUuid: request.params.inviteUuid, }) - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.json(result.data, result.status) } @@ -43,30 +46,36 @@ export class BaseSubscriptionInvitesController extends BaseHttpController { } async inviteToSubscriptionSharing(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.subscriptionInvitesController.invite({ ...request.body, - inviterEmail: response.locals.user.email, - inviterUuid: response.locals.user.uuid, - inviterRoles: response.locals.roles.map((role: Role) => role.name), + inviterEmail: locals.user.email, + inviterUuid: locals.user.uuid, + inviterRoles: locals.roles.map((role: Role) => role.name), }) return this.json(result.data, result.status) } async cancelSubscriptionSharing(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.subscriptionInvitesController.cancelInvite({ ...request.body, inviteUuid: request.params.inviteUuid, - inviterEmail: response.locals.user.email, + inviterEmail: locals.user.email, }) return this.json(result.data, result.status) } async listInvites(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.subscriptionInvitesController.listInvites({ ...request.body, - inviterEmail: response.locals.user.email, + inviterEmail: locals.user.email, }) return this.json(result.data, result.status) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts index e740a1bdb..262178f50 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts @@ -6,6 +6,7 @@ import { GetSubscriptionSetting } from '../../../Domain/UseCase/GetSubscriptionS import { GetSharedOrRegularSubscriptionForUser } from '../../../Domain/UseCase/GetSharedOrRegularSubscriptionForUser/GetSharedOrRegularSubscriptionForUser' import { SubscriptionSetting } from '../../../Domain/Setting/SubscriptionSetting' import { SubscriptionSettingHttpRepresentation } from '../../../Mapping/Http/SubscriptionSettingHttpRepresentation' +import { ResponseLocals } from '../ResponseLocals' export class BaseSubscriptionSettingsController extends BaseHttpController { constructor( @@ -22,8 +23,10 @@ export class BaseSubscriptionSettingsController extends BaseHttpController { } async getSubscriptionSetting(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) if (subscriptionOrError.isFailed()) { return this.json( diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts index 1011562d8..0471b8dd5 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts @@ -9,6 +9,7 @@ import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscript import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { User } from '../../../Domain/User/User' import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting' +import { ResponseLocals } from '../ResponseLocals' export class BaseSubscriptionTokensController extends BaseHttpController { constructor( @@ -29,7 +30,9 @@ export class BaseSubscriptionTokensController extends BaseHttpController { } async createToken(_request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -42,7 +45,7 @@ export class BaseSubscriptionTokensController extends BaseHttpController { } const result = await this.createSubscriptionToken.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) return this.json({ diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUserRequestsController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUserRequestsController.ts index 303a50b1a..d4a623083 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUserRequestsController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUserRequestsController.ts @@ -3,6 +3,7 @@ import { BaseHttpController, results } from 'inversify-express-utils' import { Request, Response } from 'express' import { UserRequestsController } from '../../../Controller/UserRequestsController' +import { ResponseLocals } from '../ResponseLocals' export class BaseUserRequestsController extends BaseHttpController { constructor( @@ -17,10 +18,12 @@ export class BaseUserRequestsController extends BaseHttpController { } async submitRequest(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.userRequestsController.submitUserRequest({ requestType: request.body.requestType, - userUuid: response.locals.user.uuid, - userEmail: response.locals.user.email, + userUuid: locals.user.uuid, + userEmail: locals.user.email, }) return this.json(result.data, result.status) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts index 1b67aff30..8fa203bc9 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts @@ -7,12 +7,11 @@ import { ClearLoginAttempts } from '../../../Domain/UseCase/ClearLoginAttempts' import { DeleteAccount } from '../../../Domain/UseCase/DeleteAccount/DeleteAccount' import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription/GetUserSubscription' import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts' -import { UpdateUser } from '../../../Domain/UseCase/UpdateUser' import { ErrorTag } from '@standardnotes/responses' +import { ResponseLocals } from '../ResponseLocals' export class BaseUsersController extends BaseHttpController { constructor( - protected updateUser: UpdateUser, protected doDeleteAccount: DeleteAccount, protected doGetUserSubscription: GetUserSubscription, protected clearLoginAttempts: ClearLoginAttempts, @@ -23,61 +22,16 @@ export class BaseUsersController extends BaseHttpController { super() if (this.controllerContainer !== undefined) { - this.controllerContainer.register('auth.users.update', this.update.bind(this)) this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this)) this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this)) this.controllerContainer.register('auth.users.delete', this.deleteAccount.bind(this)) } } - async update(request: Request, response: Response): Promise { - 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: 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 { - if (request.params.userUuid !== response.locals.user.uuid) { + const locals = response.locals as ResponseLocals + + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { @@ -107,7 +61,9 @@ export class BaseUsersController extends BaseHttpController { } async getSubscription(request: Request, response: Response): Promise { - if (request.params.userUuid !== response.locals.user.uuid) { + const locals = response.locals as ResponseLocals + + if (request.params.userUuid !== locals.user.uuid) { return this.json( { error: { @@ -130,7 +86,9 @@ export class BaseUsersController extends BaseHttpController { } async changeCredentials(request: Request, response: Response): Promise { - if (response.locals.readOnlyAccess) { + const locals = response.locals as ResponseLocals + + if (locals.readOnlyAccess) { return this.json( { error: { @@ -175,7 +133,7 @@ export class BaseUsersController extends BaseHttpController { 400, ) } - const usernameOrError = Username.create(response.locals.user.email) + const usernameOrError = Username.create(locals.user.email) if (usernameOrError.isFailed()) { return this.json( { @@ -202,7 +160,7 @@ export class BaseUsersController extends BaseHttpController { }) if (changeCredentialsResult.isFailed()) { - await this.increaseLoginAttempts.execute({ email: response.locals.user.email }) + await this.increaseLoginAttempts.execute({ email: locals.user.email }) return this.json( { @@ -214,9 +172,9 @@ export class BaseUsersController extends BaseHttpController { ) } - await this.clearLoginAttempts.execute({ email: response.locals.user.email }) + await this.clearLoginAttempts.execute({ email: locals.user.email }) - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.json(changeCredentialsResult.getValue()) } diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseValetTokenController.ts b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseValetTokenController.ts index db9ab6182..15fb15f99 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseValetTokenController.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Base/BaseValetTokenController.ts @@ -6,6 +6,7 @@ import { ValetTokenOperation } from '@standardnotes/security' import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken' import { CreateValetTokenPayload } from '../../../Domain/ValetToken/CreateValetTokenPayload' +import { ResponseLocals } from '../ResponseLocals' export class BaseValetTokenController extends BaseHttpController { constructor( @@ -20,9 +21,11 @@ export class BaseValetTokenController extends BaseHttpController { } public async create(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const payload: CreateValetTokenPayload = request.body - if (response.locals.readOnlyAccess && payload.operation !== 'read') { + if (locals.readOnlyAccess && payload.operation !== 'read') { return this.json( { error: { @@ -50,7 +53,7 @@ export class BaseValetTokenController extends BaseHttpController { } const createValetKeyResponse = await this.createValetKey.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, operation: payload.operation as ValetTokenOperation, resources: payload.resources, }) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware.ts b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware.ts index 70a42f055..67a72b3e0 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware.ts @@ -2,6 +2,7 @@ import { CrossServiceTokenData, TokenDecoderInterface } from '@standardnotes/sec import { NextFunction, Request, Response } from 'express' import { BaseMiddleware } from 'inversify-express-utils' import { Logger } from 'winston' +import { ResponseLocals } from '../ResponseLocals' export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware { constructor( @@ -34,10 +35,12 @@ export abstract class ApiGatewayAuthMiddleware extends BaseMiddleware { return } - response.locals.user = token.user - response.locals.roles = token.roles - response.locals.session = token.session - response.locals.readOnlyAccess = token.session?.readonly_access ?? false + Object.assign(response.locals, { + user: token.user, + roles: token.roles, + session: token.session, + readOnlyAccess: token.session?.readonly_access ?? false, + } as ResponseLocals) return next() } catch (error) { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware.ts b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware.ts index 7a74cb602..454f121df 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware.ts @@ -4,6 +4,7 @@ import { inject, injectable } from 'inversify' import { BaseMiddleware } from 'inversify-express-utils' import { Logger } from 'winston' import TYPES from '../../../Bootstrap/Types' +import { OfflineResponseLocals } from '../OfflineResponseLocals' @injectable() export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware { @@ -48,8 +49,10 @@ export class ApiGatewayOfflineAuthMiddleware extends BaseMiddleware { return } - response.locals.featuresToken = token.featuresToken - response.locals.userEmail = token.userEmail + Object.assign(response.locals, { + featuresToken: token.featuresToken, + userEmail: token.userEmail, + } as OfflineResponseLocals) return next() } catch (error) { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.spec.ts b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.spec.ts index 9272e6626..4ce8db807 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.spec.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.spec.ts @@ -44,8 +44,8 @@ describe('OfflineUserAuthMiddleware', () => { await createMiddleware().handler(request, response, next) - expect(response.locals.offlineUserEmail).toEqual('test@test.com') - expect(response.locals.offlineFeaturesToken).toEqual('offline-features-token') + expect(response.locals.userEmail).toEqual('test@test.com') + expect(response.locals.featuresToken).toEqual('offline-features-token') expect(next).toHaveBeenCalled() }) diff --git a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.ts b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.ts index 3a82b6c33..f30dd5cb3 100644 --- a/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.ts +++ b/packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.ts @@ -5,6 +5,7 @@ import { Logger } from 'winston' import TYPES from '../../../Bootstrap/Types' import { OfflineSettingName } from '../../../Domain/Setting/OfflineSettingName' import { OfflineSettingRepositoryInterface } from '../../../Domain/Setting/OfflineSettingRepositoryInterface' +import { OfflineResponseLocals } from '../OfflineResponseLocals' @injectable() export class OfflineUserAuthMiddleware extends BaseMiddleware { @@ -47,8 +48,10 @@ export class OfflineUserAuthMiddleware extends BaseMiddleware { return } - response.locals.offlineUserEmail = offlineFeaturesTokenSetting.email - response.locals.offlineFeaturesToken = offlineFeaturesTokenSetting.value + Object.assign(response.locals, { + featuresToken: offlineFeaturesTokenSetting.value, + userEmail: offlineFeaturesTokenSetting.email, + } as OfflineResponseLocals) return next() } catch (error) { diff --git a/packages/auth/src/Infra/InversifyExpressUtils/OfflineResponseLocals.ts b/packages/auth/src/Infra/InversifyExpressUtils/OfflineResponseLocals.ts new file mode 100644 index 000000000..325405cad --- /dev/null +++ b/packages/auth/src/Infra/InversifyExpressUtils/OfflineResponseLocals.ts @@ -0,0 +1,4 @@ +export interface OfflineResponseLocals { + userEmail: string + featuresToken: string +} diff --git a/packages/auth/src/Infra/InversifyExpressUtils/ResponseLocals.ts b/packages/auth/src/Infra/InversifyExpressUtils/ResponseLocals.ts new file mode 100644 index 000000000..8cd123251 --- /dev/null +++ b/packages/auth/src/Infra/InversifyExpressUtils/ResponseLocals.ts @@ -0,0 +1,20 @@ +import { Role } from '@standardnotes/security' + +export interface ResponseLocals { + user: { + uuid: string + email: string + } + roles: Array + 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 +} diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseItemsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseItemsController.ts index cc5375b7c..903466694 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseItemsController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseItemsController.ts @@ -14,6 +14,7 @@ import { ItemHash } from '../../../Domain/Item/ItemHash' import { CheckForTrafficAbuse } from '../../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse' import { Metric } from '../../../Domain/Metrics/Metric' import { Logger } from 'winston' +import { ResponseLocals } from '../ResponseLocals' export class BaseItemsController extends BaseHttpController { constructor( @@ -41,15 +42,16 @@ export class BaseItemsController extends BaseHttpController { } async sync(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({ metricToCheck: Metric.NAMES.ItemOperation, - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, threshold: this.itemOperationsAbuseThreshold, timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes, }) if (checkForItemOperationsAbuseResult.isFailed()) { this.logger.warn(checkForItemOperationsAbuseResult.getError(), { - userId: response.locals.user.uuid, + userId: locals.user.uuid, }) if (this.strictAbuseProtection) { return this.json({ error: { message: checkForItemOperationsAbuseResult.getError() } }, 429) @@ -58,13 +60,13 @@ export class BaseItemsController extends BaseHttpController { const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({ metricToCheck: Metric.NAMES.ContentSizeUtilized, - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, threshold: this.payloadSizeAbuseThreshold, timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes, }) if (checkForPayloadSizeAbuseResult.isFailed()) { this.logger.warn(checkForPayloadSizeAbuseResult.getError(), { - userId: response.locals.user.uuid, + userId: locals.user.uuid, }) if (this.strictAbuseProtection) { @@ -77,7 +79,7 @@ export class BaseItemsController extends BaseHttpController { for (const itemHashInput of request.body.items) { const itemHashOrError = ItemHash.create({ ...itemHashInput, - user_uuid: response.locals.user.uuid, + user_uuid: locals.user.uuid, key_system_identifier: itemHashInput.key_system_identifier ?? null, shared_vault_uuid: itemHashInput.shared_vault_uuid ?? null, }) @@ -99,7 +101,7 @@ export class BaseItemsController extends BaseHttpController { } const syncResult = await this.syncItems.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, itemHashes, computeIntegrityHash: request.body.compute_integrity === true, syncToken: request.body.sync_token, @@ -108,10 +110,10 @@ export class BaseItemsController extends BaseHttpController { contentType: request.body.content_type, apiVersion: request.body.api ?? ApiVersion.v20161215, snjsVersion: request.headers['x-snjs-version'], - readOnlyAccess: response.locals.readOnlyAccess, - sessionUuid: response.locals.session ? response.locals.session.uuid : null, + readOnlyAccess: locals.readOnlyAccess, + sessionUuid: locals.session ? locals.session.uuid : null, sharedVaultUuids, - isFreeUser: response.locals.isFreeUser, + isFreeUser: locals.isFreeUser, }) if (syncResult.isFailed()) { return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest) @@ -125,13 +127,15 @@ export class BaseItemsController extends BaseHttpController { } async checkItemsIntegrity(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + let integrityPayloads = [] if ('integrityPayloads' in request.body) { integrityPayloads = request.body.integrityPayloads } const result = await this.checkIntegrity.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, integrityPayloads, }) @@ -145,8 +149,10 @@ export class BaseItemsController extends BaseHttpController { } async getSingleItem(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getItem.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, itemUuid: request.params.uuid, }) diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseMessagesController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseMessagesController.ts index 91aec48d5..6c00a2551 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseMessagesController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseMessagesController.ts @@ -9,6 +9,7 @@ import { SendMessageToUser } from '../../../Domain/UseCase/Messaging/SendMessage import { DeleteAllMessagesSentToUser } from '../../../Domain/UseCase/Messaging/DeleteAllMessagesSentToUser/DeleteAllMessagesSentToUser' import { DeleteMessage } from '../../../Domain/UseCase/Messaging/DeleteMessage/DeleteMessage' import { GetMessagesSentByUser } from '../../../Domain/UseCase/Messaging/GetMessagesSentByUser/GetMessagesSentByUser' +import { ResponseLocals } from '../ResponseLocals' export class BaseMessagesController extends BaseHttpController { constructor( @@ -32,8 +33,10 @@ export class BaseMessagesController extends BaseHttpController { } async getMessages(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getMessageSentToUserUseCase.execute({ - recipientUuid: response.locals.user.uuid, + recipientUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -53,8 +56,10 @@ export class BaseMessagesController extends BaseHttpController { } async getMessagesSent(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getMessagesSentByUserUseCase.execute({ - senderUuid: response.locals.user.uuid, + senderUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -74,8 +79,10 @@ export class BaseMessagesController extends BaseHttpController { } async sendMessage(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.sendMessageToUserUseCase.execute({ - senderUuid: response.locals.user.uuid, + senderUuid: locals.user.uuid, recipientUuid: request.body.recipient_uuid, encryptedMessage: request.body.encrypted_message, replaceabilityIdentifier: request.body.replaceability_identifier, @@ -98,8 +105,10 @@ export class BaseMessagesController extends BaseHttpController { } async deleteMessagesSentToUser(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.deleteMessagesSentToUserUseCase.execute({ - recipientUuid: response.locals.user.uuid, + recipientUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -117,9 +126,11 @@ export class BaseMessagesController extends BaseHttpController { } async deleteMessage(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.deleteMessageUseCase.execute({ messageUuid: request.params.messageUuid, - originatorUuid: response.locals.user.uuid, + originatorUuid: locals.user.uuid, }) if (result.isFailed()) { diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts index 8eb0dc06d..d36b61dfa 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts @@ -13,6 +13,7 @@ import { DeleteSharedVaultInvitesToUser } from '../../../Domain/UseCase/SharedVa import { GetSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentByUser/GetSharedVaultInvitesSentByUser' import { DeleteSharedVaultInvitesSentByUser } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVaultInvitesSentByUser/DeleteSharedVaultInvitesSentByUser' import { GetSharedVaultInvitesSentToUser } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultInvitesSentToUser/GetSharedVaultInvitesSentToUser' +import { ResponseLocals } from '../ResponseLocals' export class BaseSharedVaultInvitesController extends BaseHttpController { constructor( @@ -63,9 +64,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async createSharedVaultInvite(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.inviteUserToSharedVaultUseCase.execute({ sharedVaultUuid: request.params.sharedVaultUuid, - senderUuid: response.locals.user.uuid, + senderUuid: locals.user.uuid, recipientUuid: request.body.recipient_uuid, encryptedMessage: request.body.encrypted_message, permission: request.body.permission, @@ -88,10 +91,12 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async updateSharedVaultInvite(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.updateSharedVaultInviteUseCase.execute({ encryptedMessage: request.body.encrypted_message, inviteUuid: request.params.inviteUuid, - senderUuid: response.locals.user.uuid, + senderUuid: locals.user.uuid, permission: request.body.permission, }) @@ -112,9 +117,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async acceptSharedVaultInvite(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.acceptSharedVaultInviteUseCase.execute({ inviteUuid: request.params.inviteUuid, - originatorUuid: response.locals.user.uuid, + originatorUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -128,7 +135,7 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { ) } - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.json({ success: true, @@ -136,9 +143,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async declineSharedVaultInvite(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.declineSharedVaultInviteUseCase.execute({ inviteUuid: request.params.inviteUuid, - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -158,8 +167,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async deleteInboundUserInvites(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.deleteSharedVaultInvitesToUserUseCase.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -179,8 +190,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async deleteOutboundUserInvites(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -200,8 +213,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async getOutboundUserInvites(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({ - senderUuid: response.locals.user.uuid, + senderUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -221,8 +236,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async getSharedVaultInvites(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({ - senderUuid: response.locals.user.uuid, + senderUuid: locals.user.uuid, sharedVaultUuid: request.params.sharedVaultUuid, }) @@ -243,8 +260,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async getUserInvites(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getSharedVaultInvitesSentToUserUseCase.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -264,9 +283,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async deleteSharedVaultInvite(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.declineSharedVaultInviteUseCase.execute({ inviteUuid: request.params.inviteUuid, - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -286,8 +307,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController { } async deleteAllSharedVaultInvites(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, sharedVaultUuid: request.params.sharedVaultUuid, }) diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts index 410c056f1..56d260901 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts @@ -7,6 +7,7 @@ import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedV import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers' import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault' import { DesignateSurvivor } from '../../../Domain/UseCase/SharedVaults/DesignateSurvivor/DesignateSurvivor' +import { ResponseLocals } from '../ResponseLocals' export class BaseSharedVaultUsersController extends BaseHttpController { constructor( @@ -29,8 +30,10 @@ export class BaseSharedVaultUsersController extends BaseHttpController { } async getSharedVaultUsers(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.getSharedVaultUsersUseCase.execute({ - originatorUuid: response.locals.user.uuid, + originatorUuid: locals.user.uuid, sharedVaultUuid: request.params.sharedVaultUuid, }) @@ -51,10 +54,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController { } async removeUserFromSharedVault(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.removeUserFromSharedVaultUseCase.execute({ sharedVaultUuid: request.params.sharedVaultUuid, userUuid: request.params.userUuid, - originatorUuid: response.locals.user.uuid, + originatorUuid: locals.user.uuid, }) if (result.isFailed()) { @@ -76,10 +81,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController { } async designateSurvivor(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.designateSurvivorUseCase.execute({ sharedVaultUuid: request.params.sharedVaultUuid, userUuid: request.params.userUuid, - originatorUuid: response.locals.user.uuid, + originatorUuid: locals.user.uuid, }) if (result.isFailed()) { diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts index 75dc2717d..bc745f513 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts @@ -11,6 +11,7 @@ import { CreateSharedVault } from '../../../Domain/UseCase/SharedVaults/CreateSh import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation' import { DeleteSharedVault } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault' import { CreateSharedVaultFileValetToken } from '../../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken' +import { ResponseLocals } from '../ResponseLocals' export class BaseSharedVaultsController extends BaseHttpController { constructor( @@ -36,8 +37,10 @@ export class BaseSharedVaultsController extends BaseHttpController { } async getSharedVaults(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const resultOrError = await this.getSharedVaultsUseCase.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, includeDesignatedSurvivors: true, }) @@ -64,9 +67,11 @@ export class BaseSharedVaultsController extends BaseHttpController { } async createSharedVault(_request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.createSharedVaultUseCase.execute({ - userUuid: response.locals.user.uuid, - userRoleNames: response.locals.roles.map((role: Role) => role.name), + userUuid: locals.user.uuid, + userRoleNames: locals.roles.map((role: Role) => role.name), }) if (result.isFailed()) { @@ -80,7 +85,7 @@ export class BaseSharedVaultsController extends BaseHttpController { ) } - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.json({ sharedVault: this.sharedVaultHttpMapper.toProjection(result.getValue().sharedVault), @@ -89,9 +94,11 @@ export class BaseSharedVaultsController extends BaseHttpController { } async deleteSharedVault(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.deleteSharedVaultUseCase.execute({ sharedVaultUuid: request.params.sharedVaultUuid, - originatorUuid: response.locals.user.uuid, + originatorUuid: locals.user.uuid, allowSurviving: false, }) @@ -106,16 +113,18 @@ export class BaseSharedVaultsController extends BaseHttpController { ) } - response.setHeader('x-invalidate-cache', response.locals.user.uuid) + response.setHeader('x-invalidate-cache', locals.user.uuid) return this.json({ success: true }) } async createValetTokenForSharedVaultFile(request: Request, response: Response): Promise { + const locals = response.locals as ResponseLocals + const result = await this.createSharedVaultFileValetTokenUseCase.execute({ - userUuid: response.locals.user.uuid, + userUuid: locals.user.uuid, sharedVaultUuid: request.params.sharedVaultUuid, - sharedVaultOwnerUploadBytesLimit: response.locals.sharedVaultOwnerContext?.upload_bytes_limit, + sharedVaultOwnerUploadBytesLimit: locals.sharedVaultOwnerContext?.upload_bytes_limit, fileUuid: request.body.file_uuid, remoteIdentifier: request.body.remote_identifier, operation: request.body.operation, diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware.ts index 54b2bca4e..78c362b23 100644 --- a/packages/syncing-server/src/Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware.ts +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware.ts @@ -4,6 +4,7 @@ import { verify } from 'jsonwebtoken' import { CrossServiceTokenData } from '@standardnotes/security' import * as winston from 'winston' import { RoleName } from '@standardnotes/domain-core' +import { ResponseLocals } from '../ResponseLocals' export class InversifyExpressAuthMiddleware extends BaseMiddleware { constructor( @@ -25,13 +26,14 @@ export class InversifyExpressAuthMiddleware extends BaseMiddleware { const decodedToken = verify(authToken, this.authJWTSecret, { algorithms: ['HS256'] }) - response.locals.user = decodedToken.user - response.locals.roles = decodedToken.roles - response.locals.isFreeUser = - decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser - response.locals.session = decodedToken.session - response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false - response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context + Object.assign(response.locals, { + user: decodedToken.user, + roles: decodedToken.roles, + isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser, + session: decodedToken.session, + readOnlyAccess: decodedToken.session?.readonly_access ?? false, + sharedVaultOwnerContext: decodedToken.shared_vault_owner_context, + } as ResponseLocals) return next() } catch (error) { diff --git a/packages/syncing-server/src/Infra/InversifyExpressUtils/ResponseLocals.ts b/packages/syncing-server/src/Infra/InversifyExpressUtils/ResponseLocals.ts new file mode 100644 index 000000000..32fda11d8 --- /dev/null +++ b/packages/syncing-server/src/Infra/InversifyExpressUtils/ResponseLocals.ts @@ -0,0 +1,24 @@ +import { Role } from '@standardnotes/security' + +export interface ResponseLocals { + user: { + uuid: string + email: string + } + roles: Array + 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 + } +}