Selaa lähdekoodia

chore: add types for response locals (#1015)

Karol Sójko 1 vuosi sitten
vanhempi
commit
d44866b3c0
44 muutettua tiedostoa jossa 455 lisäystä ja 261 poistoa
  1. 4 1
      packages/api-gateway/bin/server.ts
  2. 14 18
      packages/api-gateway/src/Controller/AuthMiddleware.ts
  3. 9 7
      packages/api-gateway/src/Controller/GRPCWebSocketAuthMiddleware.ts
  4. 5 0
      packages/api-gateway/src/Controller/OfflineResponseLocals.ts
  5. 29 0
      packages/api-gateway/src/Controller/ResponseLocals.ts
  6. 5 0
      packages/api-gateway/src/Controller/SubscriptionResponseLocals.ts
  7. 21 13
      packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts
  8. 7 5
      packages/api-gateway/src/Controller/WebSocketAuthMiddleware.ts
  9. 6 6
      packages/api-gateway/src/Controller/v1/UsersController.ts
  10. 5 2
      packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts
  11. 12 7
      packages/api-gateway/src/Service/Http/HttpServiceProxy.ts
  12. 0 1
      packages/api-gateway/src/Service/Resolver/EndpointResolver.ts
  13. 17 9
      packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts
  14. 12 9
      packages/api-gateway/src/Service/gRPC/GRPCSyncingServerServiceProxy.ts
  15. 3 1
      packages/auth/bin/server.ts
  16. 0 1
      packages/auth/src/Bootstrap/Container.ts
  17. 1 16
      packages/auth/src/Infra/InversifyExpressUtils/AnnotatedUsersController.ts
  18. 15 6
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthController.ts
  19. 14 5
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseAuthenticatorsController.ts
  20. 4 1
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseFeaturesController.ts
  21. 6 3
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseListedController.ts
  22. 7 2
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseOfflineController.ts
  23. 15 10
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionController.ts
  24. 6 3
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSessionsController.ts
  25. 18 9
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSettingsController.ts
  26. 16 7
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionInvitesController.ts
  27. 4 1
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionSettingsController.ts
  28. 5 2
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseSubscriptionTokensController.ts
  29. 5 2
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUserRequestsController.ts
  30. 14 56
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseUsersController.ts
  31. 5 2
      packages/auth/src/Infra/InversifyExpressUtils/Base/BaseValetTokenController.ts
  32. 7 4
      packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayAuthMiddleware.ts
  33. 5 2
      packages/auth/src/Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware.ts
  34. 2 2
      packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.spec.ts
  35. 5 2
      packages/auth/src/Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware.ts
  36. 4 0
      packages/auth/src/Infra/InversifyExpressUtils/OfflineResponseLocals.ts
  37. 20 0
      packages/auth/src/Infra/InversifyExpressUtils/ResponseLocals.ts
  38. 17 11
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseItemsController.ts
  39. 16 5
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseMessagesController.ts
  40. 35 12
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts
  41. 10 3
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts
  42. 17 8
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts
  43. 9 7
      packages/syncing-server/src/Infra/InversifyExpressUtils/Middleware/InversifyExpressAuthMiddleware.ts
  44. 24 0
      packages/syncing-server/src/Infra/InversifyExpressUtils/ResponseLocals.ts

+ 4 - 1
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<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
+      const locals = response.locals as ResponseLocals
+
       logger.error(`${error.stack}`, {
         method: request.method,
         url: request.url,
         snjs: request.headers['x-snjs-version'],
         application: request.headers['x-application-version'],
-        userId: response.locals.user ? response.locals.user.uuid : undefined,
+        userId: locals.user ? locals.user.uuid : undefined,
       })
       logger.debug(
         `[URL: |${request.method}| ${request.url}][SNJS: ${request.headers['x-snjs-version']}][Application: ${

+ 14 - 18
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 = <CrossServiceTokenData>(
-        verify(response.locals.authToken, this.jwtSecret, { algorithms: ['HS256'] })
-      )
+      const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
 
       if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
         await this.crossServiceTokenCache.set({
           key: cacheKey,
-          encodedCrossServiceToken: response.locals.authToken,
+          encodedCrossServiceToken: crossServiceToken,
           expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
           userUuid: decodedToken.user.uuid,
         })
       }
 
-      response.locals.user = decodedToken.user
-      response.locals.session = decodedToken.session
-      response.locals.roles = decodedToken.roles
-      response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
-      response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
-      if (response.locals.readOnlyAccess) {
-        this.logger.debug('User operates on read-only access', {
-          codeTag: 'AuthMiddleware',
-          userId: response.locals.user.uuid,
-        })
-      }
-      response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
+      Object.assign(response.locals, {
+        authToken: crossServiceToken,
+        user: decodedToken.user,
+        session: decodedToken.session,
+        roles: decodedToken.roles,
+        sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
+        readOnlyAccess: decodedToken.session?.readonly_access ?? false,
+        isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
+        belongsToSharedVaults: decodedToken.belongs_to_shared_vaults ?? [],
+      } as ResponseLocals)
     } catch (error) {
       let detailedErrorMessage = (error as Error).message
       if (error instanceof AxiosError) {

+ 9 - 7
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 = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
 
-      response.locals.user = decodedToken.user
-      response.locals.session = decodedToken.session
-      response.locals.roles = decodedToken.roles
-      response.locals.isFreeUser =
-        decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
+      Object.assign(response.locals, {
+        authToken: crossServiceToken,
+        user: decodedToken.user,
+        session: decodedToken.session,
+        roles: decodedToken.roles,
+        isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
+        readOnlyAccess: decodedToken.session?.readonly_access ?? false,
+      } as ResponseLocals)
     } catch (error) {
       this.logger.error(
         `Could not pass the request to websocket connection validation on underlying service: ${

+ 5 - 0
packages/api-gateway/src/Controller/OfflineResponseLocals.ts

@@ -0,0 +1,5 @@
+export interface OfflineResponseLocals {
+  offlineAuthToken: string
+  userEmail: string
+  featuresToken: string
+}

+ 29 - 0
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<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
+  }
+}

+ 5 - 0
packages/api-gateway/src/Controller/SubscriptionResponseLocals.ts

@@ -0,0 +1,5 @@
+import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
+
+export interface SubscriptionResponseLocals {
+  tokenAuthenticationMethod: TokenAuthenticationMethod
+}

+ 21 - 13
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 = <OfflineUserTokenData>(
       verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
     )
 
-    response.locals.offlineUserEmail = decodedToken.userEmail
-    response.locals.offlineFeaturesToken = decodedToken.featuresToken
+    Object.assign(response.locals, {
+      offlineAuthToken: authResponse.data.authToken,
+      userEmail: decodedToken.userEmail,
+      featuresToken: decodedToken.featuresToken,
+    } as OfflineResponseLocals)
   }
 
   private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
-    response.locals.authToken = authResponse.data.authToken
-
     const decodedToken = <CrossServiceTokenData>(
       verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
     )
 
-    response.locals.user = decodedToken.user
-    response.locals.roles = decodedToken.roles
+    Object.assign(response.locals, {
+      authToken: authResponse.data.authToken,
+      user: decodedToken.user,
+      roles: decodedToken.roles,
+    } as ResponseLocals)
   }
 }

+ 7 - 5
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 = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
 
-      response.locals.user = decodedToken.user
-      response.locals.session = decodedToken.session
-      response.locals.roles = decodedToken.roles
+      Object.assign(response.locals, {
+        authToken: crossServiceToken,
+        user: decodedToken.user,
+        session: decodedToken.session,
+        roles: decodedToken.roles,
+      } as ResponseLocals)
     } catch (error) {
       const errorMessage = (error as AxiosError).isAxiosError
         ? JSON.stringify((error as AxiosError).response?.data)

+ 6 - 6
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<void> {
-    if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
+    const locals = response.locals as SubscriptionResponseLocals & ResponseLocals
+
+    if (locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
       await this.httpService.callAuthServer(
         request,
         response,
@@ -227,11 +231,7 @@ export class UsersController extends BaseHttpController {
     await this.httpService.callAuthServer(
       request,
       response,
-      this.endpointResolver.resolveEndpointOrMethodIdentifier(
-        'GET',
-        'users/:userUuid/subscription',
-        response.locals.user.uuid,
-      ),
+      this.endpointResolver.resolveEndpointOrMethodIdentifier('GET', 'users/:userUuid/subscription', locals.user.uuid),
     )
   }
 

+ 5 - 2
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<string, unknown> },
   ): void {
+    const locals = response.locals as ResponseLocals
+
     void response.status(serviceResponse.statusCode).send({
       meta: {
         auth: {
-          userUuid: response.locals.user?.uuid,
-          roles: response.locals.roles,
+          userUuid: locals.user?.uuid,
+          roles: locals.roles,
         },
         server: {
           filesServerUrl: this.filesServerUrl,

+ 12 - 7
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, unknown> | string,
   ): Promise<AxiosResponse | undefined> {
+    const locals = response.locals as ResponseLocals | OfflineResponseLocals
+
     try {
       const headers: Record<string, string> = {}
       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, unknown> | string,
   ): Promise<void> {
+    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,

+ 0 - 1
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'],

+ 17 - 9
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, unknown> | string,
   ): Promise<void> {
+    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, unknown> | string,
     retryAttempt?: number,
   ): Promise<AxiosResponse | undefined> {
+    const locals = response.locals as ResponseLocals | OfflineResponseLocals
+
     try {
       const headers: Record<string, string> = {}
       for (const headerName of Object.keys(request.headers)) {
@@ -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, unknown> | string,
   ): Promise<void> {
+    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,

+ 12 - 9
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, unknown> | string,
   ): Promise<{ status: number; data: unknown }> {
+    const locals = response.locals as ResponseLocals
+
     return new Promise((resolve, reject) => {
       try {
         const syncRequest = this.syncRequestGRPCMapper.toProjection(payload as Record<string, unknown>)
 
         const metadata = new Metadata()
-        metadata.set('x-user-uuid', response.locals.user.uuid)
+        metadata.set('x-user-uuid', locals.user.uuid)
         metadata.set('x-snjs-version', request.headers['x-snjs-version'] as string)
-        metadata.set('x-read-only-access', response.locals.readOnlyAccess ? 'true' : 'false')
-        if (response.locals.readOnlyAccess) {
+        metadata.set('x-read-only-access', locals.readOnlyAccess ? 'true' : 'false')
+        if (locals.readOnlyAccess) {
           this.logger.debug('Syncing with read-only access', {
             codeTag: 'GRPCSyncingServerServiceProxy',
-            userId: response.locals.user.uuid,
+            userId: locals.user.uuid,
           })
         }
-        if (response.locals.session) {
-          metadata.set('x-session-uuid', response.locals.session.uuid)
+        if (locals.session) {
+          metadata.set('x-session-uuid', locals.session.uuid)
         }
-        metadata.set('x-is-free-user', response.locals.isFreeUser ? 'true' : 'false')
+        metadata.set('x-is-free-user', locals.isFreeUser ? 'true' : 'false')
 
         this.syncingClient.syncItems(syncRequest, metadata, (error, syncResponse) => {
           if (error) {
@@ -52,7 +55,7 @@ export class GRPCSyncingServerServiceProxy {
             if (error.code === Status.INTERNAL) {
               this.logger.error(`Internal gRPC error: ${error.message}. Payload: ${JSON.stringify(payload)}`, {
                 codeTag: 'GRPCSyncingServerServiceProxy',
-                userId: response.locals.user.uuid,
+                userId: locals.user.uuid,
               })
             }
 
@@ -68,7 +71,7 @@ export class GRPCSyncingServerServiceProxy {
         ) {
           this.logger.error(`Internal gRPC error: ${JSON.stringify(error)}. Payload: ${JSON.stringify(payload)}`, {
             codeTag: 'GRPCSyncingServerServiceProxy.catch',
-            userId: response.locals.user.uuid,
+            userId: locals.user.uuid,
           })
         }
 

+ 3 - 1
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<string, unknown>, request: Request, response: Response, _next: NextFunction) => {
+      const locals = response.locals as ResponseLocals
       logger.error(`${error.stack}`, {
         method: request.method,
         url: request.url,
         snjs: request.headers['x-snjs-version'],
         application: request.headers['x-application-version'],
-        userId: response.locals.user ? response.locals.user.uuid : undefined,
+        userId: locals.user ? locals.user.uuid : undefined,
       })
 
       response.status(500).send({

+ 0 - 1
packages/auth/src/Bootstrap/Container.ts

@@ -1708,7 +1708,6 @@ export class ContainerConfigLoader {
         .bind<BaseUsersController>(TYPES.Auth_BaseUsersController)
         .toConstantValue(
           new BaseUsersController(
-            container.get<UpdateUser>(TYPES.Auth_UpdateUser),
             container.get<DeleteAccount>(TYPES.Auth_DeleteAccount),
             container.get<GetUserSubscription>(TYPES.Auth_GetUserSubscription),
             container.get<ClearLoginAttempts>(TYPES.Auth_ClearLoginAttempts),

+ 1 - 16
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<results.JsonResult> {
-    return super.update(request, response)
+    super(doDeleteAccount, doGetUserSubscription, clearLoginAttempts, increaseLoginAttempts, changeCredentialsUseCase)
   }
 
   @httpDelete('/:userUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)

+ 15 - 6
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<results.JsonResult> {
-    if (response.locals.session) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.session) {
       const result = await this.getUserKeyParams.execute({
-        email: response.locals.user.email,
+        email: locals.user.email,
         authenticated: true,
       })
 
@@ -145,6 +148,8 @@ export class BaseAuthController extends BaseHttpController {
   }
 
   async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     if (!request.body.code_challenge) {
       return this.json(
         {
@@ -156,9 +161,9 @@ export class BaseAuthController extends BaseHttpController {
       )
     }
 
-    if (response.locals.session) {
+    if (locals.session) {
       const result = await this.getUserKeyParams.execute({
-        email: response.locals.user.email,
+        email: locals.user.email,
         authenticated: true,
         codeChallenge: request.body.code_challenge as string,
       })
@@ -248,8 +253,10 @@ export class BaseAuthController extends BaseHttpController {
   }
 
   async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.authController.generateRecoveryCodes({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     return this.json(result.data, result.status)
@@ -280,8 +287,10 @@ export class BaseAuthController extends BaseHttpController {
   }
 
   async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.authController.signOut({
-      readOnlyAccess: response.locals.readOnlyAccess,
+      readOnlyAccess: locals.readOnlyAccess,
       authorizationHeader: <string>request.headers.authorization,
     })
 

+ 14 - 5
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.authenticatorsController.list({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     return this.json(result.data, result.status)
   }
 
   async delete(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.authenticatorsController.delete({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       authenticatorId: request.params.authenticatorId,
     })
 
@@ -47,17 +52,21 @@ export class BaseAuthenticatorsController extends BaseHttpController {
   }
 
   async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.authenticatorsController.generateRegistrationOptions({
-      username: response.locals.user.email,
-      userUuid: response.locals.user.uuid,
+      username: locals.user.email,
+      userUuid: locals.user.uuid,
     })
 
     return this.json(result.data, result.status)
   }
 
   async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.authenticatorsController.verifyRegistrationResponse({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       attestationResponse: request.body.attestationResponse,
     })
 

+ 4 - 1
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<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    const locals = response.locals as ResponseLocals
+
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {

+ 6 - 3
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<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -31,8 +34,8 @@ export class BaseListedController extends BaseHttpController {
     }
 
     await this.doCreateListedAccount.execute({
-      userUuid: response.locals.user.uuid,
-      userEmail: response.locals.user.email,
+      userUuid: locals.user.uuid,
+      userEmail: locals.user.email,
     })
 
     return this.json({

+ 7 - 2
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<results.JsonResult> {
+    const locals = response.locals as OfflineResponseLocals
+
     const result = await this.doGetUserFeatures.execute({
-      email: response.locals.offlineUserEmail,
+      email: locals.userEmail,
       offline: true,
     })
 
@@ -115,8 +118,10 @@ export class BaseOfflineController extends BaseHttpController {
   }
 
   async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as OfflineResponseLocals
+
     const result = await this.getUserOfflineSubscription.execute({
-      userEmail: response.locals.userEmail,
+      userEmail: locals.userEmail,
     })
 
     if (result.success) {

+ 15 - 10
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<results.JsonResult | results.StatusCodeResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -36,7 +39,7 @@ export class BaseSessionController extends BaseHttpController {
       )
     }
 
-    if (!request.body.uuid) {
+    if (!request.body.uuid || !locals.session) {
       return this.json(
         {
           error: {
@@ -47,7 +50,7 @@ export class BaseSessionController extends BaseHttpController {
       )
     }
 
-    if (request.body.uuid === response.locals.session.uuid) {
+    if (request.body.uuid === locals.session.uuid) {
       return this.json(
         {
           error: {
@@ -59,7 +62,7 @@ export class BaseSessionController extends BaseHttpController {
     }
 
     const useCaseResponse = await this.deleteSessionForUser.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       sessionUuid: request.body.uuid,
     })
 
@@ -74,7 +77,7 @@ export class BaseSessionController extends BaseHttpController {
       )
     }
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.statusCode(204)
   }
@@ -83,7 +86,9 @@ export class BaseSessionController extends BaseHttpController {
     _request: Request,
     response: Response,
   ): Promise<results.JsonResult | results.StatusCodeResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -95,7 +100,7 @@ export class BaseSessionController extends BaseHttpController {
       )
     }
 
-    if (!response.locals.user) {
+    if (!locals.user || !locals.session) {
       return this.json(
         {
           error: {
@@ -107,12 +112,12 @@ export class BaseSessionController extends BaseHttpController {
     }
 
     await this.deleteOtherSessionsForUser.execute({
-      userUuid: response.locals.user.uuid,
-      currentSessionUuid: response.locals.session.uuid,
+      userUuid: locals.user.uuid,
+      currentSessionUuid: locals.session.uuid,
       markAsRevoked: true,
     })
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.statusCode(204)
   }

+ 6 - 3
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<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json([])
     }
 
     const useCaseResponse = await this.getActiveSessionsForUser.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     return this.json(
@@ -80,7 +83,7 @@ export class BaseSessionsController extends BaseHttpController {
         this.sessionProjector.projectCustom(
           SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
           session,
-          response.locals.session,
+          locals.session,
         ),
       ),
     )

+ 18 - 9
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<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    const locals = response.locals as ResponseLocals
+
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {
@@ -86,7 +89,9 @@ export class BaseSettingsController extends BaseHttpController {
   }
 
   async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    const locals = response.locals as ResponseLocals
+
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {
@@ -135,7 +140,9 @@ export class BaseSettingsController extends BaseHttpController {
   }
 
   async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -147,7 +154,7 @@ export class BaseSettingsController extends BaseHttpController {
       )
     }
 
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {
@@ -163,7 +170,7 @@ export class BaseSettingsController extends BaseHttpController {
     const result = await this.setSettingValue.execute({
       settingName: name,
       value,
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       checkUserPermissions: true,
     })
 
@@ -181,8 +188,8 @@ export class BaseSettingsController extends BaseHttpController {
 
     const triggerResult = await this.triggerPostSettingUpdateActions.execute({
       updatedSettingName: setting.props.name,
-      userUuid: response.locals.user.uuid,
-      userEmail: response.locals.user.email,
+      userUuid: locals.user.uuid,
+      userEmail: locals.user.email,
       unencryptedValue: value,
     })
     if (triggerResult.isFailed()) {
@@ -196,7 +203,9 @@ export class BaseSettingsController extends BaseHttpController {
   }
 
   async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -208,7 +217,7 @@ export class BaseSettingsController extends BaseHttpController {
       )
     }
 
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {

+ 16 - 7
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.subscriptionInvitesController.acceptInvite({
       api: request.query.api as ApiVersion,
       inviteUuid: request.params.inviteUuid,
     })
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.json(result.data, result.status)
   }
@@ -43,30 +46,36 @@ export class BaseSubscriptionInvitesController extends BaseHttpController {
   }
 
   async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.subscriptionInvitesController.invite({
       ...request.body,
-      inviterEmail: response.locals.user.email,
-      inviterUuid: response.locals.user.uuid,
-      inviterRoles: response.locals.roles.map((role: Role) => role.name),
+      inviterEmail: locals.user.email,
+      inviterUuid: locals.user.uuid,
+      inviterRoles: locals.roles.map((role: Role) => role.name),
     })
 
     return this.json(result.data, result.status)
   }
 
   async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.subscriptionInvitesController.cancelInvite({
       ...request.body,
       inviteUuid: request.params.inviteUuid,
-      inviterEmail: response.locals.user.email,
+      inviterEmail: locals.user.email,
     })
 
     return this.json(result.data, result.status)
   }
 
   async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.subscriptionInvitesController.listInvites({
       ...request.body,
-      inviterEmail: response.locals.user.email,
+      inviterEmail: locals.user.email,
     })
 
     return this.json(result.data, result.status)

+ 4 - 1
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const subscriptionOrError = await this.getSharedOrRegularSubscription.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
     if (subscriptionOrError.isFailed()) {
       return this.json(

+ 5 - 2
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<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -42,7 +45,7 @@ export class BaseSubscriptionTokensController extends BaseHttpController {
     }
 
     const result = await this.createSubscriptionToken.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     return this.json({

+ 5 - 2
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.userRequestsController.submitUserRequest({
       requestType: request.body.requestType,
-      userUuid: response.locals.user.uuid,
-      userEmail: response.locals.user.email,
+      userUuid: locals.user.uuid,
+      userEmail: locals.user.email,
     })
 
     return this.json(result.data, result.status)

+ 14 - 56
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<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.ReadOnlyAccess,
-            message: 'Session has read-only access.',
-          },
-        },
-        401,
-      )
-    }
-
-    if (request.params.userId !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const updateResult = await this.updateUser.execute({
-      user: response.locals.user,
-      updatedWithUserAgent: <string>request.headers['user-agent'],
-      apiVersion: request.body.api,
-    })
-
-    if (updateResult.success) {
-      response.setHeader('x-invalidate-cache', response.locals.user.uuid)
-
-      return this.json(updateResult.authResponse)
-    }
-
-    return this.json(
-      {
-        error: {
-          message: 'Could not update user.',
-        },
-      },
-      400,
-    )
-  }
-
   async deleteAccount(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    const locals = response.locals as ResponseLocals
+
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {
@@ -107,7 +61,9 @@ export class BaseUsersController extends BaseHttpController {
   }
 
   async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
+    const locals = response.locals as ResponseLocals
+
+    if (request.params.userUuid !== locals.user.uuid) {
       return this.json(
         {
           error: {
@@ -130,7 +86,9 @@ export class BaseUsersController extends BaseHttpController {
   }
 
   async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
+    const locals = response.locals as ResponseLocals
+
+    if (locals.readOnlyAccess) {
       return this.json(
         {
           error: {
@@ -175,7 +133,7 @@ export class BaseUsersController extends BaseHttpController {
         400,
       )
     }
-    const usernameOrError = Username.create(response.locals.user.email)
+    const usernameOrError = Username.create(locals.user.email)
     if (usernameOrError.isFailed()) {
       return this.json(
         {
@@ -202,7 +160,7 @@ export class BaseUsersController extends BaseHttpController {
     })
 
     if (changeCredentialsResult.isFailed()) {
-      await this.increaseLoginAttempts.execute({ email: response.locals.user.email })
+      await this.increaseLoginAttempts.execute({ email: locals.user.email })
 
       return this.json(
         {
@@ -214,9 +172,9 @@ export class BaseUsersController extends BaseHttpController {
       )
     }
 
-    await this.clearLoginAttempts.execute({ email: response.locals.user.email })
+    await this.clearLoginAttempts.execute({ email: locals.user.email })
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.json(changeCredentialsResult.getValue())
   }

+ 5 - 2
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const payload: CreateValetTokenPayload = request.body
 
-    if (response.locals.readOnlyAccess && payload.operation !== 'read') {
+    if (locals.readOnlyAccess && payload.operation !== 'read') {
       return this.json(
         {
           error: {
@@ -50,7 +53,7 @@ export class BaseValetTokenController extends BaseHttpController {
     }
 
     const createValetKeyResponse = await this.createValetKey.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       operation: payload.operation as ValetTokenOperation,
       resources: payload.resources,
     })

+ 7 - 4
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) {

+ 5 - 2
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) {

+ 2 - 2
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()
   })

+ 5 - 2
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) {

+ 4 - 0
packages/auth/src/Infra/InversifyExpressUtils/OfflineResponseLocals.ts

@@ -0,0 +1,4 @@
+export interface OfflineResponseLocals {
+  userEmail: string
+  featuresToken: string
+}

+ 20 - 0
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<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
+}

+ 17 - 11
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<results.JsonResult> {
+    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: <string>request.headers['x-snjs-version'],
-      readOnlyAccess: response.locals.readOnlyAccess,
-      sessionUuid: response.locals.session ? response.locals.session.uuid : null,
+      readOnlyAccess: locals.readOnlyAccess,
+      sessionUuid: locals.session ? locals.session.uuid : null,
       sharedVaultUuids,
-      isFreeUser: response.locals.isFreeUser,
+      isFreeUser: locals.isFreeUser,
     })
     if (syncResult.isFailed()) {
       return this.json({ error: { message: syncResult.getError() } }, HttpStatusCode.BadRequest)
@@ -125,13 +127,15 @@ export class BaseItemsController extends BaseHttpController {
   }
 
   async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     let integrityPayloads = []
     if ('integrityPayloads' in request.body) {
       integrityPayloads = request.body.integrityPayloads
     }
 
     const result = await this.checkIntegrity.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       integrityPayloads,
     })
 
@@ -145,8 +149,10 @@ export class BaseItemsController extends BaseHttpController {
   }
 
   async getSingleItem(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getItem.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       itemUuid: request.params.uuid,
     })
 

+ 16 - 5
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getMessageSentToUserUseCase.execute({
-      recipientUuid: response.locals.user.uuid,
+      recipientUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -53,8 +56,10 @@ export class BaseMessagesController extends BaseHttpController {
   }
 
   async getMessagesSent(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getMessagesSentByUserUseCase.execute({
-      senderUuid: response.locals.user.uuid,
+      senderUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -74,8 +79,10 @@ export class BaseMessagesController extends BaseHttpController {
   }
 
   async sendMessage(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.sendMessageToUserUseCase.execute({
-      senderUuid: response.locals.user.uuid,
+      senderUuid: locals.user.uuid,
       recipientUuid: request.body.recipient_uuid,
       encryptedMessage: request.body.encrypted_message,
       replaceabilityIdentifier: request.body.replaceability_identifier,
@@ -98,8 +105,10 @@ export class BaseMessagesController extends BaseHttpController {
   }
 
   async deleteMessagesSentToUser(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.deleteMessagesSentToUserUseCase.execute({
-      recipientUuid: response.locals.user.uuid,
+      recipientUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -117,9 +126,11 @@ export class BaseMessagesController extends BaseHttpController {
   }
 
   async deleteMessage(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.deleteMessageUseCase.execute({
       messageUuid: request.params.messageUuid,
-      originatorUuid: response.locals.user.uuid,
+      originatorUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {

+ 35 - 12
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.inviteUserToSharedVaultUseCase.execute({
       sharedVaultUuid: request.params.sharedVaultUuid,
-      senderUuid: response.locals.user.uuid,
+      senderUuid: locals.user.uuid,
       recipientUuid: request.body.recipient_uuid,
       encryptedMessage: request.body.encrypted_message,
       permission: request.body.permission,
@@ -88,10 +91,12 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async updateSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.updateSharedVaultInviteUseCase.execute({
       encryptedMessage: request.body.encrypted_message,
       inviteUuid: request.params.inviteUuid,
-      senderUuid: response.locals.user.uuid,
+      senderUuid: locals.user.uuid,
       permission: request.body.permission,
     })
 
@@ -112,9 +117,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async acceptSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.acceptSharedVaultInviteUseCase.execute({
       inviteUuid: request.params.inviteUuid,
-      originatorUuid: response.locals.user.uuid,
+      originatorUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -128,7 +135,7 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
       )
     }
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.json({
       success: true,
@@ -136,9 +143,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async declineSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.declineSharedVaultInviteUseCase.execute({
       inviteUuid: request.params.inviteUuid,
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -158,8 +167,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async deleteInboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.deleteSharedVaultInvitesToUserUseCase.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -179,8 +190,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async deleteOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -200,8 +213,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async getOutboundUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
-      senderUuid: response.locals.user.uuid,
+      senderUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -221,8 +236,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async getSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getSharedVaultInvitesSentByUserUseCase.execute({
-      senderUuid: response.locals.user.uuid,
+      senderUuid: locals.user.uuid,
       sharedVaultUuid: request.params.sharedVaultUuid,
     })
 
@@ -243,8 +260,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async getUserInvites(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getSharedVaultInvitesSentToUserUseCase.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -264,9 +283,11 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async deleteSharedVaultInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.declineSharedVaultInviteUseCase.execute({
       inviteUuid: request.params.inviteUuid,
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -286,8 +307,10 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
   }
 
   async deleteAllSharedVaultInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.deleteSharedVaultInvitesSentByUserUseCase.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       sharedVaultUuid: request.params.sharedVaultUuid,
     })
 

+ 10 - 3
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.getSharedVaultUsersUseCase.execute({
-      originatorUuid: response.locals.user.uuid,
+      originatorUuid: locals.user.uuid,
       sharedVaultUuid: request.params.sharedVaultUuid,
     })
 
@@ -51,10 +54,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
   }
 
   async removeUserFromSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.removeUserFromSharedVaultUseCase.execute({
       sharedVaultUuid: request.params.sharedVaultUuid,
       userUuid: request.params.userUuid,
-      originatorUuid: response.locals.user.uuid,
+      originatorUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {
@@ -76,10 +81,12 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
   }
 
   async designateSurvivor(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.designateSurvivorUseCase.execute({
       sharedVaultUuid: request.params.sharedVaultUuid,
       userUuid: request.params.userUuid,
-      originatorUuid: response.locals.user.uuid,
+      originatorUuid: locals.user.uuid,
     })
 
     if (result.isFailed()) {

+ 17 - 8
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<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const resultOrError = await this.getSharedVaultsUseCase.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       includeDesignatedSurvivors: true,
     })
 
@@ -64,9 +67,11 @@ export class BaseSharedVaultsController extends BaseHttpController {
   }
 
   async createSharedVault(_request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.createSharedVaultUseCase.execute({
-      userUuid: response.locals.user.uuid,
-      userRoleNames: response.locals.roles.map((role: Role) => role.name),
+      userUuid: locals.user.uuid,
+      userRoleNames: locals.roles.map((role: Role) => role.name),
     })
 
     if (result.isFailed()) {
@@ -80,7 +85,7 @@ export class BaseSharedVaultsController extends BaseHttpController {
       )
     }
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.json({
       sharedVault: this.sharedVaultHttpMapper.toProjection(result.getValue().sharedVault),
@@ -89,9 +94,11 @@ export class BaseSharedVaultsController extends BaseHttpController {
   }
 
   async deleteSharedVault(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.deleteSharedVaultUseCase.execute({
       sharedVaultUuid: request.params.sharedVaultUuid,
-      originatorUuid: response.locals.user.uuid,
+      originatorUuid: locals.user.uuid,
       allowSurviving: false,
     })
 
@@ -106,16 +113,18 @@ export class BaseSharedVaultsController extends BaseHttpController {
       )
     }
 
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+    response.setHeader('x-invalidate-cache', locals.user.uuid)
 
     return this.json({ success: true })
   }
 
   async createValetTokenForSharedVaultFile(request: Request, response: Response): Promise<results.JsonResult> {
+    const locals = response.locals as ResponseLocals
+
     const result = await this.createSharedVaultFileValetTokenUseCase.execute({
-      userUuid: response.locals.user.uuid,
+      userUuid: locals.user.uuid,
       sharedVaultUuid: request.params.sharedVaultUuid,
-      sharedVaultOwnerUploadBytesLimit: response.locals.sharedVaultOwnerContext?.upload_bytes_limit,
+      sharedVaultOwnerUploadBytesLimit: locals.sharedVaultOwnerContext?.upload_bytes_limit,
       fileUuid: request.body.file_uuid,
       remoteIdentifier: request.body.remote_identifier,
       operation: request.body.operation,

+ 9 - 7
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 = <CrossServiceTokenData>verify(authToken, this.authJWTSecret, { algorithms: ['HS256'] })
 
-      response.locals.user = decodedToken.user
-      response.locals.roles = decodedToken.roles
-      response.locals.isFreeUser =
-        decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser
-      response.locals.session = decodedToken.session
-      response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
-      response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
+      Object.assign(response.locals, {
+        user: decodedToken.user,
+        roles: decodedToken.roles,
+        isFreeUser: decodedToken.roles.length === 1 && decodedToken.roles[0].name === RoleName.NAMES.CoreUser,
+        session: decodedToken.session,
+        readOnlyAccess: decodedToken.session?.readonly_access ?? false,
+        sharedVaultOwnerContext: decodedToken.shared_vault_owner_context,
+      } as ResponseLocals)
 
       return next()
     } catch (error) {

+ 24 - 0
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<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
+  }
+}