Jelajahi Sumber

feat: add files server as a service to home-server (#614)

* wip: add files server as a service to home-server

* wip: introduce home-server controllers without inversify-express-utils decorators. Move in progress

* fix(auth): move remaining home server controllers

* fix(syncing-server): home server controllers

* fix(revisions): home server controllers

* fix: specs

* fix: import for legacy controller

* fix: remove router debug
Karol Sójko 2 tahun lalu
induk
melakukan
c7d575a0ff
95 mengubah file dengan 2460 tambahan dan 1895 penghapusan
  1. 1 0
      .pnp.cjs
  2. 0 1
      packages/api-gateway/src/Bootstrap/index.ts
  3. 0 1
      packages/api-gateway/src/Controller/index.ts
  4. 48 57
      packages/auth/src/Bootstrap/Container.ts
  5. 16 19
      packages/auth/src/Bootstrap/Types.ts
  6. 121 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerAdminController.ts
  7. 299 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerAuthController.ts
  8. 74 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerAuthenticatorsController.ts
  9. 42 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerFeaturesController.ts
  10. 42 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerListedController.ts
  11. 128 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerOfflineController.ts
  12. 153 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSessionController.ts
  13. 75 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController.ts
  14. 153 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSettingsController.ts
  15. 74 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionInvitesController.ts
  16. 28 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionSettingsController.ts
  17. 106 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionTokensController.ts
  18. 28 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerUserRequestsController.ts
  19. 236 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerUsersController.ts
  20. 60 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerValetTokenController.ts
  21. 55 0
      packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController.ts
  22. 0 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAdminController.spec.ts
  23. 19 98
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAdminController.ts
  24. 28 265
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthController.ts
  25. 14 54
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts
  26. 1 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressFeaturesController.spec.ts
  27. 6 32
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressFeaturesController.ts
  28. 1 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressListedController.spec.ts
  29. 7 32
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressListedController.ts
  30. 0 7
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressOfflineController.spec.ts
  31. 26 105
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressOfflineController.ts
  32. 1 11
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.spec.ts
  33. 15 132
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.ts
  34. 0 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionsController.spec.ts
  35. 11 57
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionsController.ts
  36. 1 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSettingsController.spec.ts
  37. 18 132
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSettingsController.ts
  38. 16 55
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController.ts
  39. 1 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController.spec.ts
  40. 6 21
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController.ts
  41. 1 7
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionTokensController.spec.ts
  42. 22 87
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionTokensController.ts
  43. 7 18
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController.ts
  44. 1 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.spec.ts
  45. 28 210
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.ts
  46. 1 6
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressValetTokenController.spec.ts
  47. 6 53
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressValetTokenController.ts
  48. 7 42
      packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController.ts
  49. 1 0
      packages/domain-core/src/Domain/Service/ServiceIdentifier.ts
  50. 4 4
      packages/files/bin/server.ts
  51. 4 2
      packages/files/bin/worker.ts
  52. 1 0
      packages/files/package.json
  53. 138 108
      packages/files/src/Bootstrap/Container.ts
  54. 29 0
      packages/files/src/Bootstrap/Service.ts
  55. 41 39
      packages/files/src/Bootstrap/Types.ts
  56. 2 0
      packages/files/src/Bootstrap/index.ts
  57. 0 12
      packages/files/src/Controller/HealthCheckController.spec.ts
  58. 1 1
      packages/files/src/Domain/Event/DomainEventFactory.ts
  59. 3 3
      packages/files/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts
  60. 3 3
      packages/files/src/Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler.ts
  61. 3 3
      packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSession.ts
  62. 5 5
      packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSession.ts
  63. 2 2
      packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts
  64. 2 2
      packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved.ts
  65. 4 4
      packages/files/src/Domain/UseCase/RemoveFile/RemoveFile.ts
  66. 2 2
      packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFile.ts
  67. 3 3
      packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunk.ts
  68. 1 1
      packages/files/src/Infra/FS/FSFileDownloader.ts
  69. 1 1
      packages/files/src/Infra/FS/FSFileRemover.ts
  70. 2 2
      packages/files/src/Infra/FS/FSFileUploader.ts
  71. 9 9
      packages/files/src/Infra/InversifyExpress/InversifyExpressFilesController.spec.ts
  72. 16 16
      packages/files/src/Infra/InversifyExpress/InversifyExpressFilesController.ts
  73. 12 0
      packages/files/src/Infra/InversifyExpress/InversifyExpressHealthCheckController.spec.ts
  74. 1 1
      packages/files/src/Infra/InversifyExpress/InversifyExpressHealthCheckController.ts
  75. 0 0
      packages/files/src/Infra/InversifyExpress/Middleware/ValetTokenAuthMiddleware.spec.ts
  76. 3 3
      packages/files/src/Infra/InversifyExpress/Middleware/ValetTokenAuthMiddleware.ts
  77. 1 0
      packages/files/src/Infra/InversifyExpress/index.ts
  78. 1 1
      packages/files/src/Infra/Redis/RedisUploadRepository.ts
  79. 2 2
      packages/files/src/Infra/S3/S3FileDownloader.ts
  80. 2 2
      packages/files/src/Infra/S3/S3FileRemover.ts
  81. 2 2
      packages/files/src/Infra/S3/S3FileUploader.ts
  82. 2 0
      packages/files/src/index.ts
  83. 1 0
      packages/home-server/.env.sample
  84. 3 0
      packages/home-server/bin/server.ts
  85. 1 0
      packages/home-server/package.json
  86. 3 3
      packages/revisions/src/Bootstrap/Container.ts
  87. 1 1
      packages/revisions/src/Bootstrap/Types.ts
  88. 47 0
      packages/revisions/src/Infra/InversifyExpress/HomeServer/HomeServerRevisionsController.ts
  89. 11 33
      packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts
  90. 3 3
      packages/syncing-server/src/Bootstrap/Container.ts
  91. 1 1
      packages/syncing-server/src/Bootstrap/Types.ts
  92. 85 0
      packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts
  93. 1 13
      packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts
  94. 15 63
      packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts
  95. 2 1
      yarn.lock

+ 1 - 0
.pnp.cjs

@@ -4626,6 +4626,7 @@ const RAW_RUNTIME_STATE =
           ["@standardnotes/auth-server", "workspace:packages/auth"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
+          ["@standardnotes/files-server", "workspace:packages/files"],\
           ["@standardnotes/revisions-server", "workspace:packages/revisions"],\
           ["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
           ["@types/cors", "npm:2.8.13"],\

+ 0 - 1
packages/api-gateway/src/Bootstrap/index.ts

@@ -1,3 +1,2 @@
-export * from './Container'
 export * from './Service'
 export * from './Types'

+ 0 - 1
packages/api-gateway/src/Controller/index.ts

@@ -1,6 +1,5 @@
 export * from './AuthMiddleware'
 export * from './HealthCheckController'
-export * from './LegacyController'
 export * from './SubscriptionTokenAuthMiddleware'
 export * from './TokenAuthenticationMethod'
 export * from './WebSocketAuthMiddleware'

+ 48 - 57
packages/auth/src/Bootstrap/Container.ts

@@ -228,29 +228,28 @@ import { TypeORMEphemeralSessionRepository } from '../Infra/TypeORM/TypeORMEphem
 import { TypeORMOfflineSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMOfflineSubscriptionTokenRepository'
 import { TypeORMPKCERepository } from '../Infra/TypeORM/TypeORMPKCERepository'
 import { TypeORMSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMSubscriptionTokenRepository'
-import { InversifyExpressAuthController } from '../Infra/InversifyExpressUtils/InversifyExpressAuthController'
-import { InversifyExpressAuthenticatorsController } from '../Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController'
-import { InversifyExpressSubscriptionInvitesController } from '../Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
-import { InversifyExpressUserRequestsController } from '../Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
-import { InversifyExpressWebSocketsController } from '../Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
-import { InversifyExpressSessionsController } from '../Infra/InversifyExpressUtils/InversifyExpressSessionsController'
-import { InversifyExpressValetTokenController } from '../Infra/InversifyExpressUtils/InversifyExpressValetTokenController'
-import { InversifyExpressUsersController } from '../Infra/InversifyExpressUtils/InversifyExpressUsersController'
-import { InversifyExpressAdminController } from '../Infra/InversifyExpressUtils/InversifyExpressAdminController'
-import { InversifyExpressSubscriptionTokensController } from '../Infra/InversifyExpressUtils/InversifyExpressSubscriptionTokensController'
-import { InversifyExpressSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController'
-import { InversifyExpressSettingsController } from '../Infra/InversifyExpressUtils/InversifyExpressSettingsController'
 import { SessionMiddleware } from '../Infra/InversifyExpressUtils/Middleware/SessionMiddleware'
 import { ApiGatewayOfflineAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware'
 import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware'
 import { LockMiddleware } from '../Infra/InversifyExpressUtils/Middleware/LockMiddleware'
-import { InversifyExpressSessionController } from '../Infra/InversifyExpressUtils/InversifyExpressSessionController'
-import { InversifyExpressOfflineController } from '../Infra/InversifyExpressUtils/InversifyExpressOfflineController'
-import { InversifyExpressListedController } from '../Infra/InversifyExpressUtils/InversifyExpressListedController'
-import { InversifyExpressInternalController } from '../Infra/InversifyExpressUtils/InversifyExpressInternalController'
-import { InversifyExpressFeaturesController } from '../Infra/InversifyExpressUtils/InversifyExpressFeaturesController'
 import { RequiredCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/RequiredCrossServiceTokenMiddleware'
 import { OptionalCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OptionalCrossServiceTokenMiddleware'
+import { HomeServerSettingsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSettingsController'
+import { HomeServerAdminController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAdminController'
+import { HomeServerAuthController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAuthController'
+import { HomeServerAuthenticatorsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerAuthenticatorsController'
+import { HomeServerFeaturesController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerFeaturesController'
+import { HomeServerListedController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerListedController'
+import { HomeServerOfflineController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerOfflineController'
+import { HomeServerSessionController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionController'
+import { HomeServerSubscriptionInvitesController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionInvitesController'
+import { HomeServerSubscriptionSettingsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionSettingsController'
+import { HomeServerSubscriptionTokensController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionTokensController'
+import { HomeServerUserRequestsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerUserRequestsController'
+import { HomeServerUsersController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerUsersController'
+import { HomeServerValetTokenController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerValetTokenController'
+import { HomeServerWebSocketsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController'
+import { HomeServerSessionsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController'
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -1012,9 +1011,9 @@ export class ContainerConfigLoader {
     }
 
     container
-      .bind<InversifyExpressAuthController>(TYPES.Auth_InversifyExpressAuthController)
+      .bind<HomeServerAuthController>(TYPES.Auth_HomeServerAuthController)
       .toConstantValue(
-        new InversifyExpressAuthController(
+        new HomeServerAuthController(
           container.get(TYPES.Auth_VerifyMFA),
           container.get(TYPES.Auth_SignIn),
           container.get(TYPES.Auth_GetUserKeyParams),
@@ -1029,42 +1028,42 @@ export class ContainerConfigLoader {
     // Inversify Controllers
     if (isConfiguredForHomeServer) {
       container
-        .bind<InversifyExpressAuthenticatorsController>(TYPES.Auth_InversifyExpressAuthenticatorsController)
+        .bind<HomeServerAuthenticatorsController>(TYPES.Auth_HomeServerAuthenticatorsController)
         .toConstantValue(
-          new InversifyExpressAuthenticatorsController(
+          new HomeServerAuthenticatorsController(
             container.get(TYPES.Auth_AuthenticatorsController),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressSubscriptionInvitesController>(TYPES.Auth_InversifyExpressSubscriptionInvitesController)
+        .bind<HomeServerSubscriptionInvitesController>(TYPES.Auth_HomeServerSubscriptionInvitesController)
         .toConstantValue(
-          new InversifyExpressSubscriptionInvitesController(
+          new HomeServerSubscriptionInvitesController(
             container.get(TYPES.Auth_SubscriptionInvitesController),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressUserRequestsController>(TYPES.Auth_InversifyExpressUserRequestsController)
+        .bind<HomeServerUserRequestsController>(TYPES.Auth_HomeServerUserRequestsController)
         .toConstantValue(
-          new InversifyExpressUserRequestsController(
+          new HomeServerUserRequestsController(
             container.get(TYPES.Auth_UserRequestsController),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressWebSocketsController>(TYPES.Auth_InversifyExpressWebSocketsController)
+        .bind<HomeServerWebSocketsController>(TYPES.Auth_HomeServerWebSocketsController)
         .toConstantValue(
-          new InversifyExpressWebSocketsController(
+          new HomeServerWebSocketsController(
             container.get(TYPES.Auth_CreateCrossServiceToken),
             container.get(TYPES.Auth_WebSocketConnectionTokenDecoder),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressSessionsController>(TYPES.Auth_SessionsController)
+        .bind<HomeServerSessionsController>(TYPES.Auth_HomeServerSessionsController)
         .toConstantValue(
-          new InversifyExpressSessionsController(
+          new HomeServerSessionsController(
             container.get(TYPES.Auth_GetActiveSessionsForUser),
             container.get(TYPES.Auth_AuthenticateRequest),
             container.get(TYPES.Auth_SessionProjector),
@@ -1073,17 +1072,17 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressValetTokenController>(TYPES.Auth_InversifyExpressValetTokenController)
+        .bind<HomeServerValetTokenController>(TYPES.Auth_HomeServerValetTokenController)
         .toConstantValue(
-          new InversifyExpressValetTokenController(
+          new HomeServerValetTokenController(
             container.get(TYPES.Auth_CreateValetToken),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressUsersController>(TYPES.Auth_InversifyExpressUsersController)
+        .bind<HomeServerUsersController>(TYPES.Auth_HomeServerUsersController)
         .toConstantValue(
-          new InversifyExpressUsersController(
+          new HomeServerUsersController(
             container.get(TYPES.Auth_UpdateUser),
             container.get(TYPES.Auth_GetUserKeyParams),
             container.get(TYPES.Auth_DeleteAccount),
@@ -1095,9 +1094,9 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressAdminController>(TYPES.Auth_InversifyExpressAdminController)
+        .bind<HomeServerAdminController>(TYPES.Auth_HomeServerAdminController)
         .toConstantValue(
-          new InversifyExpressAdminController(
+          new HomeServerAdminController(
             container.get(TYPES.Auth_DeleteSetting),
             container.get(TYPES.Auth_UserRepository),
             container.get(TYPES.Auth_CreateSubscriptionToken),
@@ -1106,9 +1105,9 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressSubscriptionTokensController>(TYPES.Auth_InversifyExpressSubscriptionTokensController)
+        .bind<HomeServerSubscriptionTokensController>(TYPES.Auth_HomeServerSubscriptionTokensController)
         .toConstantValue(
-          new InversifyExpressSubscriptionTokensController(
+          new HomeServerSubscriptionTokensController(
             container.get(TYPES.Auth_CreateSubscriptionToken),
             container.get(TYPES.Auth_AuthenticateSubscriptionToken),
             container.get(TYPES.Auth_SettingService),
@@ -1120,17 +1119,17 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressSubscriptionSettingsController>(TYPES.Auth_InversifyExpressSubscriptionSettingsController)
+        .bind<HomeServerSubscriptionSettingsController>(TYPES.Auth_HomeServerSubscriptionSettingsController)
         .toConstantValue(
-          new InversifyExpressSubscriptionSettingsController(
+          new HomeServerSubscriptionSettingsController(
             container.get(TYPES.Auth_GetSetting),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressSettingsController>(TYPES.Auth_InversifyExpressSettingsController)
+        .bind<HomeServerSettingsController>(TYPES.Auth_HomeServerSettingsController)
         .toConstantValue(
-          new InversifyExpressSettingsController(
+          new HomeServerSettingsController(
             container.get(TYPES.Auth_GetSettings),
             container.get(TYPES.Auth_GetSetting),
             container.get(TYPES.Auth_UpdateSetting),
@@ -1139,9 +1138,9 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressSessionController>(TYPES.Auth_InversifyExpressSessionController)
+        .bind<HomeServerSessionController>(TYPES.Auth_HomeServerSessionController)
         .toConstantValue(
-          new InversifyExpressSessionController(
+          new HomeServerSessionController(
             container.get(TYPES.Auth_DeleteSessionForUser),
             container.get(TYPES.Auth_DeletePreviousSessionsForUser),
             container.get(TYPES.Auth_RefreshSessionToken),
@@ -1149,9 +1148,9 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressOfflineController>(TYPES.Auth_InversifyExpressOfflineController)
+        .bind<HomeServerOfflineController>(TYPES.Auth_HomeServerOfflineController)
         .toConstantValue(
-          new InversifyExpressOfflineController(
+          new HomeServerOfflineController(
             container.get(TYPES.Auth_GetUserFeatures),
             container.get(TYPES.Auth_GetUserOfflineSubscription),
             container.get(TYPES.Auth_CreateOfflineSubscriptionToken),
@@ -1163,25 +1162,17 @@ export class ContainerConfigLoader {
           ),
         )
       container
-        .bind<InversifyExpressListedController>(TYPES.Auth_InversifyExpressListedController)
+        .bind<HomeServerListedController>(TYPES.Auth_HomeServerListedController)
         .toConstantValue(
-          new InversifyExpressListedController(
+          new HomeServerListedController(
             container.get(TYPES.Auth_CreateListedAccount),
             container.get(TYPES.Auth_ControllerContainer),
           ),
         )
       container
-        .bind<InversifyExpressInternalController>(TYPES.Auth_InversifyExpressInternalController)
+        .bind<HomeServerFeaturesController>(TYPES.Auth_HomeServerFeaturesController)
         .toConstantValue(
-          new InversifyExpressInternalController(
-            container.get(TYPES.Auth_GetUserFeatures),
-            container.get(TYPES.Auth_GetSetting),
-          ),
-        )
-      container
-        .bind<InversifyExpressFeaturesController>(TYPES.Auth_InversifyExpressFeaturesController)
-        .toConstantValue(
-          new InversifyExpressFeaturesController(
+          new HomeServerFeaturesController(
             container.get(TYPES.Auth_GetUserFeatures),
             container.get(TYPES.Auth_ControllerContainer),
           ),

+ 16 - 19
packages/auth/src/Bootstrap/Types.ts

@@ -216,25 +216,22 @@ const TYPES = {
   Auth_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
   Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
   Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
-  Auth_InversifyExpressAuthController: Symbol.for('Auth_InversifyExpressAuthController'),
-  Auth_InversifyExpressAuthenticatorsController: Symbol.for('Auth_InversifyExpressAuthenticatorsController'),
-  Auth_InversifyExpressSubscriptionInvitesController: Symbol.for('Auth_InversifyExpressSubscriptionInvitesController'),
-  Auth_InversifyExpressUserRequestsController: Symbol.for('Auth_InversifyExpressUserRequestsController'),
-  Auth_InversifyExpressWebSocketsController: Symbol.for('Auth_InversifyExpressWebSocketsController'),
-  Auth_SessionsController: Symbol.for('Auth_SessionsController'),
-  Auth_InversifyExpressValetTokenController: Symbol.for('Auth_InversifyExpressValetTokenController'),
-  Auth_InversifyExpressUsersController: Symbol.for('Auth_InversifyExpressUsersController'),
-  Auth_InversifyExpressAdminController: Symbol.for('Auth_InversifyExpressAdminController'),
-  Auth_InversifyExpressSubscriptionTokensController: Symbol.for('Auth_InversifyExpressSubscriptionTokensController'),
-  Auth_InversifyExpressSubscriptionSettingsController: Symbol.for(
-    'Auth_InversifyExpressSubscriptionSettingsController',
-  ),
-  Auth_InversifyExpressSettingsController: Symbol.for('Auth_InversifyExpressSettingsController'),
-  Auth_InversifyExpressSessionController: Symbol.for('Auth_InversifyExpressSessionController'),
-  Auth_InversifyExpressOfflineController: Symbol.for('Auth_InversifyExpressOfflineController'),
-  Auth_InversifyExpressListedController: Symbol.for('Auth_InversifyExpressListedController'),
-  Auth_InversifyExpressInternalController: Symbol.for('Auth_InversifyExpressInternalController'),
-  Auth_InversifyExpressFeaturesController: Symbol.for('Auth_InversifyExpressFeaturesController'),
+  Auth_HomeServerAuthController: Symbol.for('Auth_HomeServerAuthController'),
+  Auth_HomeServerAuthenticatorsController: Symbol.for('Auth_HomeServerAuthenticatorsController'),
+  Auth_HomeServerSubscriptionInvitesController: Symbol.for('Auth_HomeServerSubscriptionInvitesController'),
+  Auth_HomeServerUserRequestsController: Symbol.for('Auth_HomeServerUserRequestsController'),
+  Auth_HomeServerWebSocketsController: Symbol.for('Auth_HomeServerWebSocketsController'),
+  Auth_HomeServerSessionsController: Symbol.for('Auth_HomeServerSessionsController'),
+  Auth_HomeServerValetTokenController: Symbol.for('Auth_HomeServerValetTokenController'),
+  Auth_HomeServerUsersController: Symbol.for('Auth_HomeServerUsersController'),
+  Auth_HomeServerAdminController: Symbol.for('Auth_HomeServerAdminController'),
+  Auth_HomeServerSubscriptionTokensController: Symbol.for('Auth_HomeServerSubscriptionTokensController'),
+  Auth_HomeServerSubscriptionSettingsController: Symbol.for('Auth_HomeServerSubscriptionSettingsController'),
+  Auth_HomeServerSettingsController: Symbol.for('Auth_HomeServerSettingsController'),
+  Auth_HomeServerSessionController: Symbol.for('Auth_HomeServerSessionController'),
+  Auth_HomeServerOfflineController: Symbol.for('Auth_HomeServerOfflineController'),
+  Auth_HomeServerListedController: Symbol.for('Auth_HomeServerListedController'),
+  Auth_HomeServerFeaturesController: Symbol.for('Auth_HomeServerFeaturesController'),
 }
 
 export default TYPES

+ 121 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerAdminController.ts

@@ -0,0 +1,121 @@
+import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { SettingName } from '@standardnotes/settings'
+import { Request } from 'express'
+
+import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
+import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
+import { DeleteSetting } from '../../../Domain/UseCase/DeleteSetting/DeleteSetting'
+import { UserRepositoryInterface } from '../../../Domain/User/UserRepositoryInterface'
+
+export class HomeServerAdminController extends BaseHttpController {
+  constructor(
+    protected doDeleteSetting: DeleteSetting,
+    protected userRepository: UserRepositoryInterface,
+    protected createSubscriptionToken: CreateSubscriptionToken,
+    protected createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('admin.getUser', this.getUser.bind(this))
+      this.controllerContainer.register('admin.deleteMFASetting', this.deleteMFASetting.bind(this))
+      this.controllerContainer.register('admin.createToken', this.createToken.bind(this))
+      this.controllerContainer.register('admin.createOfflineToken', this.createOfflineToken.bind(this))
+      this.controllerContainer.register('admin.disableEmailBackups', this.disableEmailBackups.bind(this))
+    }
+  }
+
+  async getUser(request: Request): Promise<results.JsonResult> {
+    const usernameOrError = Username.create(request.params.email ?? '')
+    if (usernameOrError.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: 'Missing email parameter.',
+          },
+        },
+        400,
+      )
+    }
+    const username = usernameOrError.getValue()
+
+    const user = await this.userRepository.findOneByUsernameOrEmail(username)
+
+    if (!user) {
+      return this.json(
+        {
+          error: {
+            message: `No user with email '${username.value}'.`,
+          },
+        },
+        400,
+      )
+    }
+
+    return this.json({
+      uuid: user.uuid,
+    })
+  }
+
+  async deleteMFASetting(request: Request): Promise<results.JsonResult> {
+    const { userUuid } = request.params
+    const { uuid, updatedAt } = request.body
+
+    const result = await this.doDeleteSetting.execute({
+      uuid,
+      userUuid,
+      settingName: SettingName.NAMES.MfaSecret,
+      timestamp: updatedAt,
+      softDelete: true,
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+
+  async createToken(request: Request): Promise<results.JsonResult> {
+    const { userUuid } = request.params
+    const result = await this.createSubscriptionToken.execute({
+      userUuid,
+    })
+
+    return this.json({
+      token: result.subscriptionToken.token,
+    })
+  }
+
+  async createOfflineToken(request: Request): Promise<results.JsonResult | results.BadRequestResult> {
+    const { email } = request.params
+    const result = await this.createOfflineSubscriptionToken.execute({
+      userEmail: email,
+    })
+
+    if (!result.success) {
+      return this.badRequest()
+    }
+
+    return this.json({
+      token: result.offlineSubscriptionToken.token,
+    })
+  }
+
+  async disableEmailBackups(request: Request): Promise<results.BadRequestErrorMessageResult | results.OkResult> {
+    const { userUuid } = request.params
+
+    const result = await this.doDeleteSetting.execute({
+      userUuid,
+      settingName: SettingName.NAMES.EmailBackupFrequency,
+    })
+
+    if (result.success) {
+      return this.ok()
+    }
+
+    return this.badRequest('No email backups found')
+  }
+}

+ 299 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerAuthController.ts

@@ -0,0 +1,299 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+import { Logger } from 'winston'
+
+import { ClearLoginAttempts } from '../../../Domain/UseCase/ClearLoginAttempts'
+import { GetUserKeyParams } from '../../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
+import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
+import { SignIn } from '../../../Domain/UseCase/SignIn'
+import { VerifyMFA } from '../../../Domain/UseCase/VerifyMFA'
+import { AuthController } from '../../../Controller/AuthController'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+export class HomeServerAuthController extends BaseHttpController {
+  constructor(
+    protected verifyMFA: VerifyMFA,
+    protected signInUseCase: SignIn,
+    protected getUserKeyParams: GetUserKeyParams,
+    protected clearLoginAttempts: ClearLoginAttempts,
+    protected increaseLoginAttempts: IncreaseLoginAttempts,
+    protected logger: Logger,
+    protected authController: AuthController,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.params', this.params.bind(this))
+      this.controllerContainer.register('auth.signIn', this.signIn.bind(this))
+      this.controllerContainer.register('auth.pkceParams', this.pkceParams.bind(this))
+      this.controllerContainer.register('auth.pkceSignIn', this.pkceSignIn.bind(this))
+      this.controllerContainer.register('auth.users.register', this.register.bind(this))
+      this.controllerContainer.register('auth.generateRecoveryCodes', this.generateRecoveryCodes.bind(this))
+      this.controllerContainer.register('auth.signInWithRecoveryCodes', this.recoveryLogin.bind(this))
+      this.controllerContainer.register('auth.recoveryKeyParams', this.recoveryParams.bind(this))
+      this.controllerContainer.register('auth.signOut', this.signOut.bind(this))
+    }
+  }
+
+  async params(request: Request, response: Response): Promise<results.JsonResult> {
+    if (response.locals.session) {
+      const result = await this.getUserKeyParams.execute({
+        email: response.locals.user.email,
+        authenticated: true,
+      })
+
+      return this.json(result.keyParams)
+    }
+
+    if (!request.query.email) {
+      return this.json(
+        {
+          error: {
+            message: 'Please provide an email address.',
+          },
+        },
+        400,
+      )
+    }
+
+    const verifyMFAResponse = await this.verifyMFA.execute({
+      email: <string>request.query.email,
+      requestParams: request.query,
+      preventOTPFromFurtherUsage: false,
+    })
+
+    if (!verifyMFAResponse.success) {
+      return this.json(
+        {
+          error: {
+            tag: verifyMFAResponse.errorTag,
+            message: verifyMFAResponse.errorMessage,
+            payload: verifyMFAResponse.errorPayload,
+          },
+        },
+        401,
+      )
+    }
+
+    const result = await this.getUserKeyParams.execute({
+      email: <string>request.query.email,
+      authenticated: false,
+    })
+
+    return this.json(result.keyParams)
+  }
+
+  async signIn(request: Request): Promise<results.JsonResult> {
+    if (!request.body.email || !request.body.password) {
+      this.logger.debug('/auth/sign_in request missing credentials: %O', request.body)
+
+      return this.json(
+        {
+          error: {
+            tag: 'invalid-auth',
+            message: 'Invalid login credentials.',
+          },
+        },
+        401,
+      )
+    }
+
+    const verifyMFAResponse = await this.verifyMFA.execute({
+      email: request.body.email,
+      requestParams: request.body,
+      preventOTPFromFurtherUsage: true,
+    })
+
+    if (!verifyMFAResponse.success) {
+      return this.json(
+        {
+          error: {
+            tag: verifyMFAResponse.errorTag,
+            message: verifyMFAResponse.errorMessage,
+            payload: verifyMFAResponse.errorPayload,
+          },
+        },
+        401,
+      )
+    }
+
+    const signInResult = await this.signInUseCase.execute({
+      apiVersion: request.body.api,
+      userAgent: <string>request.headers['user-agent'],
+      email: request.body.email,
+      password: request.body.password,
+      ephemeralSession: request.body.ephemeral ?? false,
+    })
+
+    if (!signInResult.success) {
+      await this.increaseLoginAttempts.execute({ email: request.body.email })
+
+      return this.json(
+        {
+          error: {
+            message: signInResult.errorMessage,
+          },
+        },
+        signInResult.errorCode ?? 401,
+      )
+    }
+
+    await this.clearLoginAttempts.execute({ email: request.body.email })
+
+    return this.json(signInResult.authResponse)
+  }
+
+  async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
+    if (!request.body.code_challenge) {
+      return this.json(
+        {
+          error: {
+            message: 'Please provide the code challenge parameter.',
+          },
+        },
+        400,
+      )
+    }
+
+    if (response.locals.session) {
+      const result = await this.getUserKeyParams.execute({
+        email: response.locals.user.email,
+        authenticated: true,
+        codeChallenge: request.body.code_challenge as string,
+      })
+
+      return this.json(result.keyParams)
+    }
+
+    if (!request.body.email) {
+      return this.json(
+        {
+          error: {
+            message: 'Please provide an email address.',
+          },
+        },
+        400,
+      )
+    }
+
+    const verifyMFAResponse = await this.verifyMFA.execute({
+      email: <string>request.body.email,
+      requestParams: request.body,
+      preventOTPFromFurtherUsage: true,
+    })
+
+    if (!verifyMFAResponse.success) {
+      return this.json(
+        {
+          error: {
+            tag: verifyMFAResponse.errorTag,
+            message: verifyMFAResponse.errorMessage,
+            payload: verifyMFAResponse.errorPayload,
+          },
+        },
+        401,
+      )
+    }
+
+    const result = await this.getUserKeyParams.execute({
+      email: <string>request.body.email,
+      authenticated: false,
+      codeChallenge: request.body.code_challenge as string,
+    })
+
+    return this.json(result.keyParams)
+  }
+
+  async pkceSignIn(request: Request): Promise<results.JsonResult> {
+    if (!request.body.email || !request.body.password || !request.body.code_verifier) {
+      this.logger.debug('/auth/sign_in request missing credentials: %O', request.body)
+
+      return this.json(
+        {
+          error: {
+            tag: 'invalid-auth',
+            message: 'Invalid login credentials.',
+          },
+        },
+        401,
+      )
+    }
+
+    const signInResult = await this.signInUseCase.execute({
+      apiVersion: request.body.api,
+      userAgent: <string>request.headers['user-agent'],
+      email: request.body.email,
+      password: request.body.password,
+      ephemeralSession: request.body.ephemeral ?? false,
+      codeVerifier: request.body.code_verifier,
+    })
+
+    if (!signInResult.success) {
+      await this.increaseLoginAttempts.execute({ email: request.body.email })
+
+      return this.json(
+        {
+          error: {
+            message: signInResult.errorMessage,
+          },
+        },
+        401,
+      )
+    }
+
+    await this.clearLoginAttempts.execute({ email: request.body.email })
+
+    return this.json(signInResult.authResponse)
+  }
+
+  async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authController.generateRecoveryCodes({
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async recoveryLogin(request: Request): Promise<results.JsonResult> {
+    const result = await this.authController.signInWithRecoveryCodes({
+      apiVersion: request.body.api_version,
+      userAgent: <string>request.headers['user-agent'],
+      codeVerifier: request.body.code_verifier,
+      username: request.body.username,
+      recoveryCodes: request.body.recovery_codes,
+      password: request.body.password,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async recoveryParams(request: Request): Promise<results.JsonResult> {
+    const result = await this.authController.recoveryKeyParams({
+      apiVersion: request.body.api_version,
+      username: request.body.username,
+      codeChallenge: request.body.code_challenge,
+      recoveryCodes: request.body.recovery_codes,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
+    const result = await this.authController.signOut({
+      readOnlyAccess: response.locals.readOnlyAccess,
+      authorizationHeader: <string>request.headers.authorization,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async register(request: Request): Promise<results.JsonResult> {
+    const response = await this.authController.register({
+      ...request.body,
+      userAgent: <string>request.headers['user-agent'],
+    })
+
+    return this.json(response.data, response.status)
+  }
+}

+ 74 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerAuthenticatorsController.ts

@@ -0,0 +1,74 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+
+import { AuthenticatorsController } from '../../../Controller/AuthenticatorsController'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+export class HomeServerAuthenticatorsController extends BaseHttpController {
+  constructor(
+    protected authenticatorsController: AuthenticatorsController,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.authenticators.list', this.list.bind(this))
+      this.controllerContainer.register('auth.authenticators.delete', this.delete.bind(this))
+      this.controllerContainer.register(
+        'auth.authenticators.generateRegistrationOptions',
+        this.generateRegistrationOptions.bind(this),
+      )
+      this.controllerContainer.register(
+        'auth.authenticators.verifyRegistrationResponse',
+        this.verifyRegistration.bind(this),
+      )
+      this.controllerContainer.register(
+        'auth.authenticators.generateAuthenticationOptions',
+        this.generateAuthenticationOptions.bind(this),
+      )
+    }
+  }
+
+  async list(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.list({
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async delete(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.delete({
+      userUuid: response.locals.user.uuid,
+      authenticatorId: request.params.authenticatorId,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.generateRegistrationOptions({
+      username: response.locals.user.email,
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.verifyRegistrationResponse({
+      userUuid: response.locals.user.uuid,
+      attestationResponse: request.body.attestationResponse,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async generateAuthenticationOptions(request: Request): Promise<results.JsonResult> {
+    const result = await this.authenticatorsController.generateAuthenticationOptions({
+      username: request.body.username,
+    })
+
+    return this.json(result.data, result.status)
+  }
+}

+ 42 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerFeaturesController.ts

@@ -0,0 +1,42 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+
+import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+export class HomeServerFeaturesController extends BaseHttpController {
+  constructor(
+    protected doGetUserFeatures: GetUserFeatures,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.users.getFeatures', this.getFeatures.bind(this))
+    }
+  }
+
+  async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
+    if (request.params.userUuid !== response.locals.user.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Operation not allowed.',
+          },
+        },
+        401,
+      )
+    }
+
+    const result = await this.doGetUserFeatures.execute({
+      userUuid: request.params.userUuid,
+      offline: false,
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+}

+ 42 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerListedController.ts

@@ -0,0 +1,42 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { ErrorTag } from '@standardnotes/responses'
+import { Request, Response } from 'express'
+
+import { CreateListedAccount } from '../../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+export class HomeServerListedController extends BaseHttpController {
+  constructor(
+    protected doCreateListedAccount: CreateListedAccount,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.users.createListedAccount', this.createListedAccount.bind(this))
+    }
+  }
+
+  async createListedAccount(_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,
+      )
+    }
+
+    await this.doCreateListedAccount.execute({
+      userUuid: response.locals.user.uuid,
+      userEmail: response.locals.user.email,
+    })
+
+    return this.json({
+      message: 'Listed account creation requested successfully.',
+    })
+  }
+}

+ 128 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerOfflineController.ts

@@ -0,0 +1,128 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+import { TokenEncoderInterface, OfflineUserTokenData } from '@standardnotes/security'
+import { Logger } from 'winston'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+import { AuthenticateOfflineSubscriptionToken } from '../../../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
+import { CreateOfflineSubscriptionToken } from '../../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
+import { GetUserFeatures } from '../../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
+import { GetUserOfflineSubscription } from '../../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
+
+export class HomeServerOfflineController extends BaseHttpController {
+  constructor(
+    protected doGetUserFeatures: GetUserFeatures,
+    protected getUserOfflineSubscription: GetUserOfflineSubscription,
+    protected createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
+    protected authenticateToken: AuthenticateOfflineSubscriptionToken,
+    protected tokenEncoder: TokenEncoderInterface<OfflineUserTokenData>,
+    protected jwtTTL: number,
+    protected logger: Logger,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.offline.features', this.getOfflineFeatures.bind(this))
+      this.controllerContainer.register('auth.offline.subscriptionTokens.create', this.createToken.bind(this))
+      this.controllerContainer.register('auth.users.getOfflineSubscriptionByToken', this.getSubscription.bind(this))
+    }
+  }
+
+  async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.doGetUserFeatures.execute({
+      email: response.locals.offlineUserEmail,
+      offline: true,
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+
+  async createToken(request: Request): Promise<results.JsonResult> {
+    if (!request.body.email) {
+      return this.json(
+        {
+          error: {
+            tag: 'invalid-request',
+            message: 'Invalid request parameters.',
+          },
+        },
+        400,
+      )
+    }
+
+    const response = await this.createOfflineSubscriptionToken.execute({
+      userEmail: request.body.email,
+    })
+
+    if (!response.success) {
+      return this.json({ success: false, error: { tag: response.error } })
+    }
+
+    return this.json({ success: true })
+  }
+
+  async validate(request: Request): Promise<results.JsonResult> {
+    if (!request.body.email) {
+      this.logger.debug('[Offline Subscription Token Validation] Missing email')
+
+      return this.json(
+        {
+          error: {
+            tag: 'invalid-request',
+            message: 'Invalid request parameters.',
+          },
+        },
+        400,
+      )
+    }
+
+    const authenticateTokenResponse = await this.authenticateToken.execute({
+      token: request.params.token,
+      userEmail: request.body.email,
+    })
+
+    if (!authenticateTokenResponse.success) {
+      this.logger.debug('[Offline Subscription Token Validation] invalid token')
+
+      return this.json(
+        {
+          error: {
+            tag: 'invalid-auth',
+            message: 'Invalid login credentials.',
+          },
+        },
+        401,
+      )
+    }
+
+    const offlineAuthTokenData: OfflineUserTokenData = {
+      userEmail: authenticateTokenResponse.email,
+      featuresToken: authenticateTokenResponse.featuresToken,
+    }
+
+    const authToken = this.tokenEncoder.encodeExpirableToken(offlineAuthTokenData, this.jwtTTL)
+
+    this.logger.debug(
+      `[Offline Subscription Token Validation] authenticated token for user ${authenticateTokenResponse.email}`,
+    )
+
+    return this.json({ authToken })
+  }
+
+  async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.getUserOfflineSubscription.execute({
+      userEmail: response.locals.userEmail,
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+}

+ 153 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSessionController.ts

@@ -0,0 +1,153 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { ErrorTag } from '@standardnotes/responses'
+
+import { DeletePreviousSessionsForUser } from '../../../Domain/UseCase/DeletePreviousSessionsForUser'
+import { DeleteSessionForUser } from '../../../Domain/UseCase/DeleteSessionForUser'
+import { RefreshSessionToken } from '../../../Domain/UseCase/RefreshSessionToken'
+
+export class HomeServerSessionController extends BaseHttpController {
+  constructor(
+    protected deleteSessionForUser: DeleteSessionForUser,
+    protected deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
+    protected refreshSessionToken: RefreshSessionToken,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.sessions.delete', this.deleteSession.bind(this))
+      this.controllerContainer.register('auth.sessions.deleteAll', this.deleteAllSessions.bind(this))
+      this.controllerContainer.register('auth.sessions.refresh', this.refresh.bind(this))
+    }
+  }
+
+  async deleteSession(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
+    if (response.locals.readOnlyAccess) {
+      return this.json(
+        {
+          error: {
+            tag: ErrorTag.ReadOnlyAccess,
+            message: 'Session has read-only access.',
+          },
+        },
+        401,
+      )
+    }
+
+    if (!request.body.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Please provide the session identifier.',
+          },
+        },
+        400,
+      )
+    }
+
+    if (request.body.uuid === response.locals.session.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'You can not delete your current session.',
+          },
+        },
+        400,
+      )
+    }
+
+    const useCaseResponse = await this.deleteSessionForUser.execute({
+      userUuid: response.locals.user.uuid,
+      sessionUuid: request.body.uuid,
+    })
+
+    if (!useCaseResponse.success) {
+      return this.json(
+        {
+          error: {
+            message: useCaseResponse.errorMessage,
+          },
+        },
+        400,
+      )
+    }
+
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
+    return this.statusCode(204)
+  }
+
+  async deleteAllSessions(
+    _request: Request,
+    response: Response,
+  ): Promise<results.JsonResult | results.StatusCodeResult> {
+    if (response.locals.readOnlyAccess) {
+      return this.json(
+        {
+          error: {
+            tag: ErrorTag.ReadOnlyAccess,
+            message: 'Session has read-only access.',
+          },
+        },
+        401,
+      )
+    }
+
+    if (!response.locals.user) {
+      return this.json(
+        {
+          error: {
+            message: 'No session exists with the provided identifier.',
+          },
+        },
+        401,
+      )
+    }
+
+    await this.deletePreviousSessionsForUser.execute({
+      userUuid: response.locals.user.uuid,
+      currentSessionUuid: response.locals.session.uuid,
+    })
+
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
+    return this.statusCode(204)
+  }
+
+  async refresh(request: Request, response: Response): Promise<results.JsonResult> {
+    if (!request.body.access_token || !request.body.refresh_token) {
+      return this.json(
+        {
+          error: {
+            message: 'Please provide all required parameters.',
+          },
+        },
+        400,
+      )
+    }
+
+    const result = await this.refreshSessionToken.execute({
+      accessToken: request.body.access_token,
+      refreshToken: request.body.refresh_token,
+    })
+
+    if (!result.success) {
+      return this.json(
+        {
+          error: {
+            tag: result.errorTag,
+            message: result.errorMessage,
+          },
+        },
+        400,
+      )
+    }
+
+    response.setHeader('x-invalidate-cache', result.userUuid as string)
+    return this.json({
+      session: result.sessionPayload,
+    })
+  }
+}

+ 75 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSessionsController.ts

@@ -0,0 +1,75 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+
+import { AuthenticateRequest } from '../../../Domain/UseCase/AuthenticateRequest'
+import { CreateCrossServiceToken } from '../../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
+import { GetActiveSessionsForUser } from '../../../Domain/UseCase/GetActiveSessionsForUser'
+import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
+import { Session } from '../../../Domain/Session/Session'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { User } from '../../../Domain/User/User'
+import { SessionProjector } from '../../../Projection/SessionProjector'
+
+export class HomeServerSessionsController extends BaseHttpController {
+  constructor(
+    protected getActiveSessionsForUser: GetActiveSessionsForUser,
+    protected authenticateRequest: AuthenticateRequest,
+    protected sessionProjector: ProjectorInterface<Session>,
+    protected createCrossServiceToken: CreateCrossServiceToken,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.sessions.list', this.getSessions.bind(this))
+      this.controllerContainer.register('auth.sessions.validate', this.validate.bind(this))
+    }
+  }
+
+  async validate(request: Request): Promise<results.JsonResult> {
+    const authenticateRequestResponse = await this.authenticateRequest.execute({
+      authorizationHeader: request.headers.authorization,
+    })
+
+    if (!authenticateRequestResponse.success) {
+      return this.json(
+        {
+          error: {
+            tag: authenticateRequestResponse.errorTag,
+            message: authenticateRequestResponse.errorMessage,
+          },
+        },
+        authenticateRequestResponse.responseCode,
+      )
+    }
+
+    const user = authenticateRequestResponse.user as User
+
+    const result = await this.createCrossServiceToken.execute({
+      user,
+      session: authenticateRequestResponse.session,
+    })
+
+    return this.json({ authToken: result.token })
+  }
+
+  async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
+    if (response.locals.readOnlyAccess) {
+      return this.json([])
+    }
+
+    const useCaseResponse = await this.getActiveSessionsForUser.execute({
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(
+      useCaseResponse.sessions.map((session) =>
+        this.sessionProjector.projectCustom(
+          SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
+          session,
+          response.locals.session,
+        ),
+      ),
+    )
+  }
+}

+ 153 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSettingsController.ts

@@ -0,0 +1,153 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { ErrorTag } from '@standardnotes/responses'
+import { Request, Response } from 'express'
+
+import { DeleteSetting } from '../../../Domain/UseCase/DeleteSetting/DeleteSetting'
+import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
+import { GetSettings } from '../../../Domain/UseCase/GetSettings/GetSettings'
+import { UpdateSetting } from '../../../Domain/UseCase/UpdateSetting/UpdateSetting'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { EncryptionVersion } from '../../../Domain/Encryption/EncryptionVersion'
+
+export class HomeServerSettingsController extends BaseHttpController {
+  constructor(
+    protected doGetSettings: GetSettings,
+    protected doGetSetting: GetSetting,
+    protected doUpdateSetting: UpdateSetting,
+    protected doDeleteSetting: DeleteSetting,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.users.getSettings', this.getSettings.bind(this))
+      this.controllerContainer.register('auth.users.getSetting', this.getSetting.bind(this))
+      this.controllerContainer.register('auth.users.updateSetting', this.updateSetting.bind(this))
+      this.controllerContainer.register('auth.users.deleteSetting', this.deleteSetting.bind(this))
+    }
+  }
+
+  async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
+    if (request.params.userUuid !== response.locals.user.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Operation not allowed.',
+          },
+        },
+        401,
+      )
+    }
+
+    const { userUuid } = request.params
+    const result = await this.doGetSettings.execute({ userUuid })
+
+    return this.json(result)
+  }
+
+  async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
+    if (request.params.userUuid !== response.locals.user.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Operation not allowed.',
+          },
+        },
+        401,
+      )
+    }
+
+    const { userUuid, settingName } = request.params
+    const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+
+  async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
+    if (response.locals.readOnlyAccess) {
+      return this.json(
+        {
+          error: {
+            tag: ErrorTag.ReadOnlyAccess,
+            message: 'Session has read-only access.',
+          },
+        },
+        401,
+      )
+    }
+
+    if (request.params.userUuid !== response.locals.user.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Operation not allowed.',
+          },
+        },
+        401,
+      )
+    }
+
+    const { name, value, serverEncryptionVersion = EncryptionVersion.Default, sensitive = false } = request.body
+
+    const props = {
+      name,
+      unencryptedValue: value,
+      serverEncryptionVersion,
+      sensitive,
+    }
+
+    const { userUuid } = request.params
+    const result = await this.doUpdateSetting.execute({
+      userUuid,
+      props,
+    })
+
+    if (result.success) {
+      return this.json({ setting: result.setting }, result.statusCode)
+    }
+
+    return this.json(result, result.statusCode)
+  }
+
+  async deleteSetting(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.userUuid !== response.locals.user.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Operation not allowed.',
+          },
+        },
+        401,
+      )
+    }
+
+    const { userUuid, settingName } = request.params
+
+    const result = await this.doDeleteSetting.execute({
+      userUuid,
+      settingName,
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+}

+ 74 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionInvitesController.ts

@@ -0,0 +1,74 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { ApiVersion } from '@standardnotes/api'
+
+import { SubscriptionInvitesController } from '../../../Controller/SubscriptionInvitesController'
+import { Role } from '../../../Domain/Role/Role'
+
+export class HomeServerSubscriptionInvitesController extends BaseHttpController {
+  constructor(
+    protected subscriptionInvitesController: SubscriptionInvitesController,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.subscriptionInvites.accept', this.acceptInvite.bind(this))
+      this.controllerContainer.register('auth.subscriptionInvites.declineInvite', this.declineInvite.bind(this))
+      this.controllerContainer.register('auth.subscriptionInvites.create', this.inviteToSubscriptionSharing.bind(this))
+      this.controllerContainer.register('auth.subscriptionInvites.delete', this.cancelSubscriptionSharing.bind(this))
+      this.controllerContainer.register('auth.subscriptionInvites.list', this.listInvites.bind(this))
+    }
+  }
+
+  async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    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)
+
+    return this.json(result.data, result.status)
+  }
+
+  async declineInvite(request: Request): Promise<results.JsonResult> {
+    const response = await this.subscriptionInvitesController.declineInvite({
+      api: request.query.api as ApiVersion,
+      inviteUuid: request.params.inviteUuid,
+    })
+
+    return this.json(response.data, response.status)
+  }
+
+  async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
+    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),
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.subscriptionInvitesController.cancelInvite({
+      ...request.body,
+      inviteUuid: request.params.inviteUuid,
+      inviterEmail: response.locals.user.email,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.subscriptionInvitesController.listInvites({
+      ...request.body,
+      inviterEmail: response.locals.user.email,
+    })
+
+    return this.json(result.data, result.status)
+  }
+}

+ 28 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionSettingsController.ts

@@ -0,0 +1,28 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { Request, Response } from 'express'
+
+import { GetSetting } from '../../../Domain/UseCase/GetSetting/GetSetting'
+
+export class HomeServerSubscriptionSettingsController extends BaseHttpController {
+  constructor(protected doGetSetting: GetSetting, private controllerContainer?: ControllerContainerInterface) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.users.getSubscriptionSetting', this.getSubscriptionSetting.bind(this))
+    }
+  }
+
+  async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.doGetSetting.execute({
+      userUuid: response.locals.user.uuid,
+      settingName: request.params.subscriptionSettingName.toUpperCase(),
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+}

+ 106 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerSubscriptionTokensController.ts

@@ -0,0 +1,106 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { ErrorTag } from '@standardnotes/responses'
+import { Role, TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { Request, Response } from 'express'
+
+import { SettingServiceInterface } from '../../../Domain/Setting/SettingServiceInterface'
+import { AuthenticateSubscriptionToken } from '../../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken'
+import { CreateSubscriptionToken } from '../../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
+import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
+import { SettingName } from '@standardnotes/settings'
+import { User } from '../../../Domain/User/User'
+
+export class HomeServerSubscriptionTokensController extends BaseHttpController {
+  constructor(
+    protected createSubscriptionToken: CreateSubscriptionToken,
+    protected authenticateToken: AuthenticateSubscriptionToken,
+    protected settingService: SettingServiceInterface,
+    protected userProjector: ProjectorInterface<User>,
+    protected roleProjector: ProjectorInterface<Role>,
+    protected tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
+    protected jwtTTL: number,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.subscription-tokens.create', this.createToken.bind(this))
+    }
+  }
+
+  async createToken(_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,
+      )
+    }
+
+    const result = await this.createSubscriptionToken.execute({
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json({
+      token: result.subscriptionToken.token,
+    })
+  }
+
+  async validate(request: Request): Promise<results.JsonResult> {
+    const authenticateTokenResponse = await this.authenticateToken.execute({
+      token: request.params.token,
+    })
+
+    if (!authenticateTokenResponse.success) {
+      return this.json(
+        {
+          error: {
+            tag: 'invalid-auth',
+            message: 'Invalid login credentials.',
+          },
+        },
+        401,
+      )
+    }
+
+    const user = authenticateTokenResponse.user as User
+    let extensionKey = undefined
+    const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({
+      settingName: SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
+      userUuid: user.uuid,
+    })
+    if (extensionKeySetting !== null) {
+      extensionKey = extensionKeySetting.value as string
+    }
+
+    const roles = await user.roles
+
+    const authTokenData: CrossServiceTokenData = {
+      user: await this.projectUser(user),
+      roles: await this.projectRoles(roles),
+      extensionKey,
+    }
+
+    const authToken = this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL)
+
+    return this.json({ authToken })
+  }
+
+  private async projectUser(user: User): Promise<{ uuid: string; email: string }> {
+    return <{ uuid: string; email: string }>await this.userProjector.projectSimple(user)
+  }
+
+  private async projectRoles(roles: Array<Role>): Promise<Array<{ uuid: string; name: string }>> {
+    const roleProjections = []
+    for (const role of roles) {
+      roleProjections.push(<{ uuid: string; name: string }>await this.roleProjector.projectSimple(role))
+    }
+
+    return roleProjections
+  }
+}

+ 28 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerUserRequestsController.ts

@@ -0,0 +1,28 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { Request, Response } from 'express'
+
+import { UserRequestsController } from '../../../Controller/UserRequestsController'
+
+export class HomeServerUserRequestsController extends BaseHttpController {
+  constructor(
+    protected userRequestsController: UserRequestsController,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.users.createRequest', this.submitRequest.bind(this))
+    }
+  }
+
+  async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.userRequestsController.submitUserRequest({
+      requestType: request.body.requestType,
+      userUuid: response.locals.user.uuid,
+      userEmail: response.locals.user.email,
+    })
+
+    return this.json(result.data, result.status)
+  }
+}

+ 236 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerUsersController.ts

@@ -0,0 +1,236 @@
+import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+import { ChangeCredentials } from '../../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
+import { ClearLoginAttempts } from '../../../Domain/UseCase/ClearLoginAttempts'
+import { DeleteAccount } from '../../../Domain/UseCase/DeleteAccount/DeleteAccount'
+import { GetUserKeyParams } from '../../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
+import { GetUserSubscription } from '../../../Domain/UseCase/GetUserSubscription/GetUserSubscription'
+import { IncreaseLoginAttempts } from '../../../Domain/UseCase/IncreaseLoginAttempts'
+import { UpdateUser } from '../../../Domain/UseCase/UpdateUser'
+import { ErrorTag } from '@standardnotes/responses'
+
+export class HomeServerUsersController extends BaseHttpController {
+  constructor(
+    protected updateUser: UpdateUser,
+    protected getUserKeyParams: GetUserKeyParams,
+    protected doDeleteAccount: DeleteAccount,
+    protected doGetUserSubscription: GetUserSubscription,
+    protected clearLoginAttempts: ClearLoginAttempts,
+    protected increaseLoginAttempts: IncreaseLoginAttempts,
+    protected changeCredentialsUseCase: ChangeCredentials,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.users.update', this.update.bind(this))
+      this.controllerContainer.register('auth.users.getKeyParams', this.keyParams.bind(this))
+      this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
+      this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.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,
+      pwFunc: request.body.pw_func,
+      pwAlg: request.body.pw_alg,
+      pwCost: request.body.pw_cost,
+      pwKeySize: request.body.pw_key_size,
+      pwNonce: request.body.pw_nonce,
+      pwSalt: request.body.pw_salt,
+      kpOrigination: request.body.origination,
+      kpCreated: request.body.created,
+      version: request.body.version,
+    })
+
+    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 keyParams(request: Request): Promise<results.JsonResult> {
+    const email = 'email' in request.query ? <string>request.query.email : undefined
+    const userUuid = 'uuid' in request.query ? <string>request.query.uuid : undefined
+
+    if (!email && !userUuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Missing mandatory request query parameters.',
+          },
+        },
+        400,
+      )
+    }
+
+    const result = await this.getUserKeyParams.execute({
+      email,
+      userUuid,
+      authenticated: request.query.authenticated === 'true',
+    })
+
+    return this.json(result.keyParams)
+  }
+
+  async deleteAccount(request: Request): Promise<results.JsonResult> {
+    const result = await this.doDeleteAccount.execute({
+      email: request.params.email,
+    })
+
+    return this.json({ message: result.message }, result.responseCode)
+  }
+
+  async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
+    if (request.params.userUuid !== response.locals.user.uuid) {
+      return this.json(
+        {
+          error: {
+            message: 'Operation not allowed.',
+          },
+        },
+        401,
+      )
+    }
+
+    const result = await this.doGetUserSubscription.execute({
+      userUuid: request.params.userUuid,
+    })
+
+    if (result.success) {
+      return this.json(result)
+    }
+
+    return this.json(result, 400)
+  }
+
+  async changeCredentials(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.body.current_password) {
+      return this.json(
+        {
+          error: {
+            message:
+              'Your current password is required to change your password. Please update your application if you do not see this option.',
+          },
+        },
+        400,
+      )
+    }
+
+    if (!request.body.new_password) {
+      return this.json(
+        {
+          error: {
+            message: 'Your new password is required to change your password. Please try again.',
+          },
+        },
+        400,
+      )
+    }
+
+    if (!request.body.pw_nonce) {
+      return this.json(
+        {
+          error: {
+            message: 'The change password request is missing new auth parameters. Please try again.',
+          },
+        },
+        400,
+      )
+    }
+    const usernameOrError = Username.create(response.locals.user.email)
+    if (usernameOrError.isFailed()) {
+      return this.json(
+        {
+          error: {
+            message: 'Invalid username.',
+          },
+        },
+        400,
+      )
+    }
+    const username = usernameOrError.getValue()
+
+    const changeCredentialsResult = await this.changeCredentialsUseCase.execute({
+      username,
+      apiVersion: request.body.api,
+      currentPassword: request.body.current_password,
+      newPassword: request.body.new_password,
+      newEmail: request.body.new_email,
+      pwNonce: request.body.pw_nonce,
+      kpCreated: request.body.created,
+      kpOrigination: request.body.origination,
+      updatedWithUserAgent: <string>request.headers['user-agent'],
+      protocolVersion: request.body.version,
+    })
+
+    if (!changeCredentialsResult.success) {
+      await this.increaseLoginAttempts.execute({ email: response.locals.user.email })
+
+      return this.json(
+        {
+          error: {
+            message: changeCredentialsResult.errorMessage,
+          },
+        },
+        401,
+      )
+    }
+
+    await this.clearLoginAttempts.execute({ email: response.locals.user.email })
+
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
+    return this.json(changeCredentialsResult.authResponse)
+  }
+}

+ 60 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerValetTokenController.ts

@@ -0,0 +1,60 @@
+import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
+import { Request, Response } from 'express'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+import { CreateValetToken } from '../../../Domain/UseCase/CreateValetToken/CreateValetToken'
+import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
+import { ValetTokenOperation } from '@standardnotes/security'
+
+export class HomeServerValetTokenController extends BaseHttpController {
+  constructor(protected createValetKey: CreateValetToken, private controllerContainer?: ControllerContainerInterface) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.valet-tokens.create', this.create.bind(this))
+    }
+  }
+
+  public async create(request: Request, response: Response): Promise<results.JsonResult> {
+    const payload: CreateValetTokenPayload = request.body
+
+    if (response.locals.readOnlyAccess && payload.operation !== 'read') {
+      return this.json(
+        {
+          error: {
+            tag: ErrorTag.ReadOnlyAccess,
+            message: 'Session has read-only access.',
+          },
+        },
+        401,
+      )
+    }
+
+    for (const resource of payload.resources) {
+      const resourceUuidOrError = Uuid.create(resource.remoteIdentifier)
+      if (resourceUuidOrError.isFailed()) {
+        return this.json(
+          {
+            error: {
+              tag: ErrorTag.ParametersInvalid,
+              message: 'Invalid remote resource identifier.',
+            },
+          },
+          400,
+        )
+      }
+    }
+
+    const createValetKeyResponse = await this.createValetKey.execute({
+      userUuid: response.locals.user.uuid,
+      operation: payload.operation as ValetTokenOperation,
+      resources: payload.resources,
+    })
+
+    if (!createValetKeyResponse.success) {
+      return this.json(createValetKeyResponse, 403)
+    }
+
+    return this.json(createValetKeyResponse)
+  }
+}

+ 55 - 0
packages/auth/src/Infra/InversifyExpressUtils/HomeServer/HomeServerWebSocketsController.ts

@@ -0,0 +1,55 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
+import { Request } from 'express'
+import { BaseHttpController, results } from 'inversify-express-utils'
+
+import { CreateCrossServiceToken } from '../../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
+import { ErrorTag } from '@standardnotes/responses'
+
+export class HomeServerWebSocketsController extends BaseHttpController {
+  constructor(
+    protected createCrossServiceToken: CreateCrossServiceToken,
+    protected tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('auth.webSockets.validateToken', this.validateToken.bind(this))
+    }
+  }
+
+  async validateToken(request: Request): Promise<results.JsonResult> {
+    if (!request.headers.authorization) {
+      return this.json(
+        {
+          error: {
+            tag: ErrorTag.AuthInvalid,
+            message: 'Invalid authorization token.',
+          },
+        },
+        401,
+      )
+    }
+
+    const token: WebSocketConnectionTokenData | undefined = this.tokenDecoder.decodeToken(request.headers.authorization)
+
+    if (token === undefined) {
+      return this.json(
+        {
+          error: {
+            tag: ErrorTag.AuthInvalid,
+            message: 'Invalid authorization token.',
+          },
+        },
+        401,
+      )
+    }
+
+    const result = await this.createCrossServiceToken.execute({
+      userUuid: token.userUuid,
+    })
+
+    return this.json({ authToken: result.token })
+  }
+}

+ 0 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAdminController.spec.ts

@@ -8,7 +8,6 @@ import * as express from 'express'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 describe('InversifyExpressAdminController', () => {
   let deleteSetting: DeleteSetting
@@ -17,7 +16,6 @@ describe('InversifyExpressAdminController', () => {
   let createOfflineSubscriptionToken: CreateOfflineSubscriptionToken
   let request: express.Request
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
   const createController = () =>
     new InversifyExpressAdminController(
@@ -25,7 +23,6 @@ describe('InversifyExpressAdminController', () => {
       userRepository,
       createSubscriptionToken,
       createOfflineSubscriptionToken,
-      controllerContainer,
     )
 
   beforeEach(() => {
@@ -58,9 +55,6 @@ describe('InversifyExpressAdminController', () => {
       body: {},
       params: {},
     } as jest.Mocked<express.Request>
-
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
   })
 
   it('should return error if missing email parameter', async () => {

+ 19 - 98
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAdminController.ts

@@ -1,9 +1,6 @@
-import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
-import { SettingName } from '@standardnotes/settings'
 import { Request } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpDelete,
   httpGet,
@@ -12,124 +9,48 @@ import {
   results,
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
+import { HomeServerAdminController } from './HomeServer/HomeServerAdminController'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { UserRepositoryInterface } from '../../Domain/User/UserRepositoryInterface'
 
 @controller('/admin')
-export class InversifyExpressAdminController extends BaseHttpController {
+export class InversifyExpressAdminController extends HomeServerAdminController {
   constructor(
-    @inject(TYPES.Auth_DeleteSetting) private doDeleteSetting: DeleteSetting,
-    @inject(TYPES.Auth_UserRepository) private userRepository: UserRepositoryInterface,
-    @inject(TYPES.Auth_CreateSubscriptionToken) private createSubscriptionToken: CreateSubscriptionToken,
+    @inject(TYPES.Auth_DeleteSetting) override doDeleteSetting: DeleteSetting,
+    @inject(TYPES.Auth_UserRepository) override userRepository: UserRepositoryInterface,
+    @inject(TYPES.Auth_CreateSubscriptionToken) override createSubscriptionToken: CreateSubscriptionToken,
     @inject(TYPES.Auth_CreateOfflineSubscriptionToken)
-    private createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    override createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
   ) {
-    super()
-
-    this.controllerContainer.register('admin.getUser', this.getUser.bind(this))
-    this.controllerContainer.register('admin.deleteMFASetting', this.deleteMFASetting.bind(this))
-    this.controllerContainer.register('admin.createToken', this.createToken.bind(this))
-    this.controllerContainer.register('admin.createOfflineToken', this.createOfflineToken.bind(this))
-    this.controllerContainer.register('admin.disableEmailBackups', this.disableEmailBackups.bind(this))
+    super(doDeleteSetting, userRepository, createSubscriptionToken, createOfflineSubscriptionToken)
   }
 
   @httpGet('/user/:email')
-  async getUser(request: Request): Promise<results.JsonResult> {
-    const usernameOrError = Username.create(request.params.email ?? '')
-    if (usernameOrError.isFailed()) {
-      return this.json(
-        {
-          error: {
-            message: 'Missing email parameter.',
-          },
-        },
-        400,
-      )
-    }
-    const username = usernameOrError.getValue()
-
-    const user = await this.userRepository.findOneByUsernameOrEmail(username)
-
-    if (!user) {
-      return this.json(
-        {
-          error: {
-            message: `No user with email '${username.value}'.`,
-          },
-        },
-        400,
-      )
-    }
-
-    return this.json({
-      uuid: user.uuid,
-    })
+  override async getUser(request: Request): Promise<results.JsonResult> {
+    return super.getUser(request)
   }
 
   @httpDelete('/users/:userUuid/mfa')
-  async deleteMFASetting(request: Request): Promise<results.JsonResult> {
-    const { userUuid } = request.params
-    const { uuid, updatedAt } = request.body
-
-    const result = await this.doDeleteSetting.execute({
-      uuid,
-      userUuid,
-      settingName: SettingName.NAMES.MfaSecret,
-      timestamp: updatedAt,
-      softDelete: true,
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async deleteMFASetting(request: Request): Promise<results.JsonResult> {
+    return super.deleteMFASetting(request)
   }
 
   @httpPost('/users/:userUuid/subscription-token')
-  async createToken(request: Request): Promise<results.JsonResult> {
-    const { userUuid } = request.params
-    const result = await this.createSubscriptionToken.execute({
-      userUuid,
-    })
-
-    return this.json({
-      token: result.subscriptionToken.token,
-    })
+  override async createToken(request: Request): Promise<results.JsonResult> {
+    return super.createToken(request)
   }
 
   @httpPost('/users/:email/offline-subscription-token')
-  async createOfflineToken(request: Request): Promise<results.JsonResult | results.BadRequestResult> {
-    const { email } = request.params
-    const result = await this.createOfflineSubscriptionToken.execute({
-      userEmail: email,
-    })
-
-    if (!result.success) {
-      return this.badRequest()
-    }
-
-    return this.json({
-      token: result.offlineSubscriptionToken.token,
-    })
+  override async createOfflineToken(request: Request): Promise<results.JsonResult | results.BadRequestResult> {
+    return super.createOfflineToken(request)
   }
 
   @httpPost('/users/:userUuid/email-backups')
-  async disableEmailBackups(request: Request): Promise<results.BadRequestErrorMessageResult | results.OkResult> {
-    const { userUuid } = request.params
-
-    const result = await this.doDeleteSetting.execute({
-      userUuid,
-      settingName: SettingName.NAMES.EmailBackupFrequency,
-    })
-
-    if (result.success) {
-      return this.ok()
-    }
-
-    return this.badRequest('No email backups found')
+  override async disableEmailBackups(
+    request: Request,
+  ): Promise<results.BadRequestErrorMessageResult | results.OkResult> {
+    return super.disableEmailBackups(request)
   }
 }

+ 28 - 265
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthController.ts

@@ -1,6 +1,5 @@
 import { Request, Response } from 'express'
 import {
-  BaseHttpController,
   controller,
   httpGet,
   httpPost,
@@ -16,301 +15,65 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
 import { Logger } from 'winston'
 import { GetUserKeyParams } from '../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
 import { AuthController } from '../../Controller/AuthController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
+import { HomeServerAuthController } from './HomeServer/HomeServerAuthController'
 
 @controller('/auth')
-export class InversifyExpressAuthController extends BaseHttpController {
+export class InversifyExpressAuthController extends HomeServerAuthController {
   constructor(
-    @inject(TYPES.Auth_VerifyMFA) private verifyMFA: VerifyMFA,
-    @inject(TYPES.Auth_SignIn) private signInUseCase: SignIn,
-    @inject(TYPES.Auth_GetUserKeyParams) private getUserKeyParams: GetUserKeyParams,
-    @inject(TYPES.Auth_ClearLoginAttempts) private clearLoginAttempts: ClearLoginAttempts,
-    @inject(TYPES.Auth_IncreaseLoginAttempts) private increaseLoginAttempts: IncreaseLoginAttempts,
-    @inject(TYPES.Auth_Logger) private logger: Logger,
-    @inject(TYPES.Auth_AuthController) private authController: AuthController,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    @inject(TYPES.Auth_VerifyMFA) override verifyMFA: VerifyMFA,
+    @inject(TYPES.Auth_SignIn) override signInUseCase: SignIn,
+    @inject(TYPES.Auth_GetUserKeyParams) override getUserKeyParams: GetUserKeyParams,
+    @inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
+    @inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
+    @inject(TYPES.Auth_Logger) override logger: Logger,
+    @inject(TYPES.Auth_AuthController) override authController: AuthController,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.params', this.params.bind(this))
-    this.controllerContainer.register('auth.signIn', this.signIn.bind(this))
-    this.controllerContainer.register('auth.pkceParams', this.pkceParams.bind(this))
-    this.controllerContainer.register('auth.pkceSignIn', this.pkceSignIn.bind(this))
-    this.controllerContainer.register('auth.users.register', this.register.bind(this))
-    this.controllerContainer.register('auth.generateRecoveryCodes', this.generateRecoveryCodes.bind(this))
-    this.controllerContainer.register('auth.signInWithRecoveryCodes', this.recoveryLogin.bind(this))
-    this.controllerContainer.register('auth.recoveryKeyParams', this.recoveryParams.bind(this))
-    this.controllerContainer.register('auth.signOut', this.signOut.bind(this))
+    super(verifyMFA, signInUseCase, getUserKeyParams, clearLoginAttempts, increaseLoginAttempts, logger, authController)
   }
 
   @httpGet('/params', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
-  async params(request: Request, response: Response): Promise<results.JsonResult> {
-    if (response.locals.session) {
-      const result = await this.getUserKeyParams.execute({
-        email: response.locals.user.email,
-        authenticated: true,
-      })
-
-      return this.json(result.keyParams)
-    }
-
-    if (!request.query.email) {
-      return this.json(
-        {
-          error: {
-            message: 'Please provide an email address.',
-          },
-        },
-        400,
-      )
-    }
-
-    const verifyMFAResponse = await this.verifyMFA.execute({
-      email: <string>request.query.email,
-      requestParams: request.query,
-      preventOTPFromFurtherUsage: false,
-    })
-
-    if (!verifyMFAResponse.success) {
-      return this.json(
-        {
-          error: {
-            tag: verifyMFAResponse.errorTag,
-            message: verifyMFAResponse.errorMessage,
-            payload: verifyMFAResponse.errorPayload,
-          },
-        },
-        401,
-      )
-    }
-
-    const result = await this.getUserKeyParams.execute({
-      email: <string>request.query.email,
-      authenticated: false,
-    })
-
-    return this.json(result.keyParams)
+  override async params(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.params(request, response)
   }
 
   @httpPost('/sign_in', TYPES.Auth_LockMiddleware)
-  async signIn(request: Request): Promise<results.JsonResult> {
-    if (!request.body.email || !request.body.password) {
-      this.logger.debug('/auth/sign_in request missing credentials: %O', request.body)
-
-      return this.json(
-        {
-          error: {
-            tag: 'invalid-auth',
-            message: 'Invalid login credentials.',
-          },
-        },
-        401,
-      )
-    }
-
-    const verifyMFAResponse = await this.verifyMFA.execute({
-      email: request.body.email,
-      requestParams: request.body,
-      preventOTPFromFurtherUsage: true,
-    })
-
-    if (!verifyMFAResponse.success) {
-      return this.json(
-        {
-          error: {
-            tag: verifyMFAResponse.errorTag,
-            message: verifyMFAResponse.errorMessage,
-            payload: verifyMFAResponse.errorPayload,
-          },
-        },
-        401,
-      )
-    }
-
-    const signInResult = await this.signInUseCase.execute({
-      apiVersion: request.body.api,
-      userAgent: <string>request.headers['user-agent'],
-      email: request.body.email,
-      password: request.body.password,
-      ephemeralSession: request.body.ephemeral ?? false,
-    })
-
-    if (!signInResult.success) {
-      await this.increaseLoginAttempts.execute({ email: request.body.email })
-
-      return this.json(
-        {
-          error: {
-            message: signInResult.errorMessage,
-          },
-        },
-        signInResult.errorCode ?? 401,
-      )
-    }
-
-    await this.clearLoginAttempts.execute({ email: request.body.email })
-
-    return this.json(signInResult.authResponse)
+  override async signIn(request: Request): Promise<results.JsonResult> {
+    return super.signIn(request)
   }
 
   @httpPost('/pkce_params', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
-  async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
-    if (!request.body.code_challenge) {
-      return this.json(
-        {
-          error: {
-            message: 'Please provide the code challenge parameter.',
-          },
-        },
-        400,
-      )
-    }
-
-    if (response.locals.session) {
-      const result = await this.getUserKeyParams.execute({
-        email: response.locals.user.email,
-        authenticated: true,
-        codeChallenge: request.body.code_challenge as string,
-      })
-
-      return this.json(result.keyParams)
-    }
-
-    if (!request.body.email) {
-      return this.json(
-        {
-          error: {
-            message: 'Please provide an email address.',
-          },
-        },
-        400,
-      )
-    }
-
-    const verifyMFAResponse = await this.verifyMFA.execute({
-      email: <string>request.body.email,
-      requestParams: request.body,
-      preventOTPFromFurtherUsage: true,
-    })
-
-    if (!verifyMFAResponse.success) {
-      return this.json(
-        {
-          error: {
-            tag: verifyMFAResponse.errorTag,
-            message: verifyMFAResponse.errorMessage,
-            payload: verifyMFAResponse.errorPayload,
-          },
-        },
-        401,
-      )
-    }
-
-    const result = await this.getUserKeyParams.execute({
-      email: <string>request.body.email,
-      authenticated: false,
-      codeChallenge: request.body.code_challenge as string,
-    })
-
-    return this.json(result.keyParams)
+  override async pkceParams(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.pkceParams(request, response)
   }
 
   @httpPost('/pkce_sign_in', TYPES.Auth_LockMiddleware)
-  async pkceSignIn(request: Request): Promise<results.JsonResult> {
-    if (!request.body.email || !request.body.password || !request.body.code_verifier) {
-      this.logger.debug('/auth/sign_in request missing credentials: %O', request.body)
-
-      return this.json(
-        {
-          error: {
-            tag: 'invalid-auth',
-            message: 'Invalid login credentials.',
-          },
-        },
-        401,
-      )
-    }
-
-    const signInResult = await this.signInUseCase.execute({
-      apiVersion: request.body.api,
-      userAgent: <string>request.headers['user-agent'],
-      email: request.body.email,
-      password: request.body.password,
-      ephemeralSession: request.body.ephemeral ?? false,
-      codeVerifier: request.body.code_verifier,
-    })
-
-    if (!signInResult.success) {
-      await this.increaseLoginAttempts.execute({ email: request.body.email })
-
-      return this.json(
-        {
-          error: {
-            message: signInResult.errorMessage,
-          },
-        },
-        401,
-      )
-    }
-
-    await this.clearLoginAttempts.execute({ email: request.body.email })
-
-    return this.json(signInResult.authResponse)
+  override async pkceSignIn(request: Request): Promise<results.JsonResult> {
+    return super.pkceSignIn(request)
   }
 
   @httpPost('/recovery/codes', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.authController.generateRecoveryCodes({
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(result.data, result.status)
+  override async generateRecoveryCodes(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.generateRecoveryCodes(_request, response)
   }
 
   @httpPost('/recovery/login', TYPES.Auth_LockMiddleware)
-  async recoveryLogin(request: Request): Promise<results.JsonResult> {
-    const result = await this.authController.signInWithRecoveryCodes({
-      apiVersion: request.body.api_version,
-      userAgent: <string>request.headers['user-agent'],
-      codeVerifier: request.body.code_verifier,
-      username: request.body.username,
-      recoveryCodes: request.body.recovery_codes,
-      password: request.body.password,
-    })
-
-    return this.json(result.data, result.status)
+  override async recoveryLogin(request: Request): Promise<results.JsonResult> {
+    return super.recoveryLogin(request)
   }
 
   @httpPost('/recovery/params')
-  async recoveryParams(request: Request): Promise<results.JsonResult> {
-    const result = await this.authController.recoveryKeyParams({
-      apiVersion: request.body.api_version,
-      username: request.body.username,
-      codeChallenge: request.body.code_challenge,
-      recoveryCodes: request.body.recovery_codes,
-    })
-
-    return this.json(result.data, result.status)
+  override async recoveryParams(request: Request): Promise<results.JsonResult> {
+    return super.recoveryParams(request)
   }
 
   @httpPost('/sign_out', TYPES.Auth_OptionalCrossServiceTokenMiddleware)
-  async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
-    const result = await this.authController.signOut({
-      readOnlyAccess: response.locals.readOnlyAccess,
-      authorizationHeader: <string>request.headers.authorization,
-    })
-
-    return this.json(result.data, result.status)
+  override async signOut(request: Request, response: Response): Promise<results.JsonResult | void> {
+    return super.signOut(request, response)
   }
 
   @httpPost('/')
-  async register(request: Request): Promise<results.JsonResult> {
-    const response = await this.authController.register({
-      ...request.body,
-      userAgent: <string>request.headers['user-agent'],
-    })
-
-    return this.json(response.data, response.status)
+  override async register(request: Request): Promise<results.JsonResult> {
+    return super.register(request)
   }
 }

+ 14 - 54
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController.ts

@@ -1,6 +1,5 @@
 import { Request, Response } from 'express'
 import {
-  BaseHttpController,
   controller,
   httpDelete,
   httpGet,
@@ -10,78 +9,39 @@ import {
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import { AuthenticatorsController } from '../../Controller/AuthenticatorsController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
+import { HomeServerAuthenticatorsController } from './HomeServer/HomeServerAuthenticatorsController'
 
 @controller('/authenticators')
-export class InversifyExpressAuthenticatorsController extends BaseHttpController {
+export class InversifyExpressAuthenticatorsController extends HomeServerAuthenticatorsController {
   constructor(
-    @inject(TYPES.Auth_AuthenticatorsController) private authenticatorsController: AuthenticatorsController,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    @inject(TYPES.Auth_AuthenticatorsController) override authenticatorsController: AuthenticatorsController,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.authenticators.list', this.list.bind(this))
-    this.controllerContainer.register('auth.authenticators.delete', this.delete.bind(this))
-    this.controllerContainer.register(
-      'auth.authenticators.generateRegistrationOptions',
-      this.generateRegistrationOptions.bind(this),
-    )
-    this.controllerContainer.register(
-      'auth.authenticators.verifyRegistrationResponse',
-      this.verifyRegistration.bind(this),
-    )
-    this.controllerContainer.register(
-      'auth.authenticators.generateAuthenticationOptions',
-      this.generateAuthenticationOptions.bind(this),
-    )
+    super(authenticatorsController)
   }
 
   @httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async list(_request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.authenticatorsController.list({
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(result.data, result.status)
+  override async list(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.list(_request, response)
   }
 
   @httpDelete('/:authenticatorId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async delete(request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.authenticatorsController.delete({
-      userUuid: response.locals.user.uuid,
-      authenticatorId: request.params.authenticatorId,
-    })
-
-    return this.json(result.data, result.status)
+  override async delete(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.delete(request, response)
   }
 
   @httpGet('/generate-registration-options', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.authenticatorsController.generateRegistrationOptions({
-      username: response.locals.user.email,
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(result.data, result.status)
+  override async generateRegistrationOptions(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.generateRegistrationOptions(_request, response)
   }
 
   @httpPost('/verify-registration', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.authenticatorsController.verifyRegistrationResponse({
-      userUuid: response.locals.user.uuid,
-      attestationResponse: request.body.attestationResponse,
-    })
-
-    return this.json(result.data, result.status)
+  override async verifyRegistration(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.verifyRegistration(request, response)
   }
 
   @httpPost('/generate-authentication-options')
-  async generateAuthenticationOptions(request: Request): Promise<results.JsonResult> {
-    const result = await this.authenticatorsController.generateAuthenticationOptions({
-      username: request.body.username,
-    })
-
-    return this.json(result.data, result.status)
+  override async generateAuthenticationOptions(request: Request): Promise<results.JsonResult> {
+    return super.generateAuthenticationOptions(request)
   }
 }

+ 1 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressFeaturesController.spec.ts

@@ -6,7 +6,6 @@ import { InversifyExpressFeaturesController } from './InversifyExpressFeaturesCo
 import { results } from 'inversify-express-utils'
 import { User } from '../../Domain/User/User'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 describe('InversifyExpressFeaturesController', () => {
   let getUserFeatures: GetUserFeatures
@@ -14,14 +13,10 @@ describe('InversifyExpressFeaturesController', () => {
   let request: express.Request
   let response: express.Response
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
-  const createController = () => new InversifyExpressFeaturesController(getUserFeatures, controllerContainer)
+  const createController = () => new InversifyExpressFeaturesController(getUserFeatures)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user.uuid = '123'
 

+ 6 - 32
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressFeaturesController.ts

@@ -1,7 +1,6 @@
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpGet,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9,41 +8,16 @@ import {
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerFeaturesController } from './HomeServer/HomeServerFeaturesController'
 
 @controller('/users/:userUuid/features')
-export class InversifyExpressFeaturesController extends BaseHttpController {
-  constructor(
-    @inject(TYPES.Auth_GetUserFeatures) private doGetUserFeatures: GetUserFeatures,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
-  ) {
-    super()
-
-    this.controllerContainer.register('auth.users.getFeatures', this.getFeatures.bind(this))
+export class InversifyExpressFeaturesController extends HomeServerFeaturesController {
+  constructor(@inject(TYPES.Auth_GetUserFeatures) override doGetUserFeatures: GetUserFeatures) {
+    super(doGetUserFeatures)
   }
 
   @httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const result = await this.doGetUserFeatures.execute({
-      userUuid: request.params.userUuid,
-      offline: false,
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async getFeatures(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getFeatures(request, response)
   }
 }

+ 1 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressListedController.spec.ts

@@ -6,7 +6,6 @@ import { results } from 'inversify-express-utils'
 import { InversifyExpressListedController } from './InversifyExpressListedController'
 import { User } from '../../Domain/User/User'
 import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 describe('InversifyExpressListedController', () => {
   let createListedAccount: CreateListedAccount
@@ -14,14 +13,10 @@ describe('InversifyExpressListedController', () => {
   let request: express.Request
   let response: express.Response
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
-  const createController = () => new InversifyExpressListedController(createListedAccount, controllerContainer)
+  const createController = () => new InversifyExpressListedController(createListedAccount)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user.uuid = '123'
 

+ 7 - 32
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressListedController.ts

@@ -1,44 +1,19 @@
 import { inject } from 'inversify'
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { BaseHttpController, controller, httpPost, results } from 'inversify-express-utils'
+import { controller, httpPost, results } from 'inversify-express-utils'
 import { Request, Response } from 'express'
 import TYPES from '../../Bootstrap/Types'
 import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
-import { ErrorTag } from '@standardnotes/responses'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerListedController } from './HomeServer/HomeServerListedController'
 
 @controller('/listed')
-export class InversifyExpressListedController extends BaseHttpController {
-  constructor(
-    @inject(TYPES.Auth_CreateListedAccount) private doCreateListedAccount: CreateListedAccount,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
-  ) {
-    super()
-
-    this.controllerContainer.register('auth.users.createListedAccount', this.createListedAccount.bind(this))
+export class InversifyExpressListedController extends HomeServerListedController {
+  constructor(@inject(TYPES.Auth_CreateListedAccount) override doCreateListedAccount: CreateListedAccount) {
+    super(doCreateListedAccount)
   }
 
   @httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async createListedAccount(_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,
-      )
-    }
-
-    await this.doCreateListedAccount.execute({
-      userUuid: response.locals.user.uuid,
-      userEmail: response.locals.user.email,
-    })
-
-    return this.json({
-      message: 'Listed account creation requested successfully.',
-    })
+  override async createListedAccount(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.createListedAccount(_request, response)
   }
 }

+ 0 - 7
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressOfflineController.spec.ts

@@ -14,7 +14,6 @@ import { GetUserOfflineSubscription } from '../../Domain/UseCase/GetUserOfflineS
 import { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/security'
 import { SubscriptionName } from '@standardnotes/common'
 import { Logger } from 'winston'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 describe('InversifyExpressOfflineController', () => {
   let getUserFeatures: GetUserFeatures
@@ -29,8 +28,6 @@ describe('InversifyExpressOfflineController', () => {
   let response: express.Response
   let user: User
 
-  let controllerContainer: ControllerContainerInterface
-
   const createController = () =>
     new InversifyExpressOfflineController(
       getUserFeatures,
@@ -40,13 +37,9 @@ describe('InversifyExpressOfflineController', () => {
       tokenEncoder,
       jwtTTL,
       logger,
-      controllerContainer,
     )
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user.uuid = '123'
 

+ 26 - 105
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressOfflineController.ts

@@ -1,7 +1,6 @@
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpGet,
   httpPost,
@@ -10,132 +9,54 @@ import {
 } from 'inversify-express-utils'
 import { Logger } from 'winston'
 import { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/security'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import TYPES from '../../Bootstrap/Types'
 import { AuthenticateOfflineSubscriptionToken } from '../../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
 import { GetUserOfflineSubscription } from '../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
+import { HomeServerOfflineController } from './HomeServer/HomeServerOfflineController'
 
 @controller('/offline')
-export class InversifyExpressOfflineController extends BaseHttpController {
+export class InversifyExpressOfflineController extends HomeServerOfflineController {
   constructor(
-    @inject(TYPES.Auth_GetUserFeatures) private doGetUserFeatures: GetUserFeatures,
-    @inject(TYPES.Auth_GetUserOfflineSubscription) private getUserOfflineSubscription: GetUserOfflineSubscription,
+    @inject(TYPES.Auth_GetUserFeatures) override doGetUserFeatures: GetUserFeatures,
+    @inject(TYPES.Auth_GetUserOfflineSubscription) override getUserOfflineSubscription: GetUserOfflineSubscription,
     @inject(TYPES.Auth_CreateOfflineSubscriptionToken)
-    private createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
+    override createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
     @inject(TYPES.Auth_AuthenticateOfflineSubscriptionToken)
-    private authenticateToken: AuthenticateOfflineSubscriptionToken,
-    @inject(TYPES.Auth_OfflineUserTokenEncoder) private tokenEncoder: TokenEncoderInterface<OfflineUserTokenData>,
-    @inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
-    @inject(TYPES.Auth_Logger) private logger: Logger,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    override authenticateToken: AuthenticateOfflineSubscriptionToken,
+    @inject(TYPES.Auth_OfflineUserTokenEncoder) override tokenEncoder: TokenEncoderInterface<OfflineUserTokenData>,
+    @inject(TYPES.Auth_AUTH_JWT_TTL) override jwtTTL: number,
+    @inject(TYPES.Auth_Logger) override logger: Logger,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.offline.features', this.getOfflineFeatures.bind(this))
-    this.controllerContainer.register('auth.offline.subscriptionTokens.create', this.createToken.bind(this))
-    this.controllerContainer.register('auth.users.getOfflineSubscriptionByToken', this.getSubscription.bind(this))
+    super(
+      doGetUserFeatures,
+      getUserOfflineSubscription,
+      createOfflineSubscriptionToken,
+      authenticateToken,
+      tokenEncoder,
+      jwtTTL,
+      logger,
+    )
   }
 
   @httpGet('/features', TYPES.Auth_OfflineUserAuthMiddleware)
-  async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.doGetUserFeatures.execute({
-      email: response.locals.offlineUserEmail,
-      offline: true,
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async getOfflineFeatures(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getOfflineFeatures(_request, response)
   }
 
   @httpPost('/subscription-tokens')
-  async createToken(request: Request): Promise<results.JsonResult> {
-    if (!request.body.email) {
-      return this.json(
-        {
-          error: {
-            tag: 'invalid-request',
-            message: 'Invalid request parameters.',
-          },
-        },
-        400,
-      )
-    }
-
-    const response = await this.createOfflineSubscriptionToken.execute({
-      userEmail: request.body.email,
-    })
-
-    if (!response.success) {
-      return this.json({ success: false, error: { tag: response.error } })
-    }
-
-    return this.json({ success: true })
+  override async createToken(request: Request): Promise<results.JsonResult> {
+    return super.createToken(request)
   }
 
   @httpPost('/subscription-tokens/:token/validate')
-  async validate(request: Request): Promise<results.JsonResult> {
-    if (!request.body.email) {
-      this.logger.debug('[Offline Subscription Token Validation] Missing email')
-
-      return this.json(
-        {
-          error: {
-            tag: 'invalid-request',
-            message: 'Invalid request parameters.',
-          },
-        },
-        400,
-      )
-    }
-
-    const authenticateTokenResponse = await this.authenticateToken.execute({
-      token: request.params.token,
-      userEmail: request.body.email,
-    })
-
-    if (!authenticateTokenResponse.success) {
-      this.logger.debug('[Offline Subscription Token Validation] invalid token')
-
-      return this.json(
-        {
-          error: {
-            tag: 'invalid-auth',
-            message: 'Invalid login credentials.',
-          },
-        },
-        401,
-      )
-    }
-
-    const offlineAuthTokenData: OfflineUserTokenData = {
-      userEmail: authenticateTokenResponse.email,
-      featuresToken: authenticateTokenResponse.featuresToken,
-    }
-
-    const authToken = this.tokenEncoder.encodeExpirableToken(offlineAuthTokenData, this.jwtTTL)
-
-    this.logger.debug(
-      `[Offline Subscription Token Validation] authenticated token for user ${authenticateTokenResponse.email}`,
-    )
-
-    return this.json({ authToken })
+  override async validate(request: Request): Promise<results.JsonResult> {
+    return super.validate(request)
   }
 
   @httpGet('/users/subscription', TYPES.Auth_ApiGatewayOfflineAuthMiddleware)
-  async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.getUserOfflineSubscription.execute({
-      userEmail: response.locals.userEmail,
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async getSubscription(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSubscription(_request, response)
   }
 }

+ 1 - 11
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.spec.ts

@@ -4,7 +4,6 @@ import * as express from 'express'
 
 import { InversifyExpressSessionController } from './InversifyExpressSessionController'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
 import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
 import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
@@ -15,20 +14,11 @@ describe('InversifyExpressSessionController', () => {
   let refreshSessionToken: RefreshSessionToken
   let request: express.Request
   let response: express.Response
-  let controllerContainer: ControllerContainerInterface
 
   const createController = () =>
-    new InversifyExpressSessionController(
-      deleteSessionForUser,
-      deletePreviousSessionsForUser,
-      refreshSessionToken,
-      controllerContainer,
-    )
+    new InversifyExpressSessionController(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     deleteSessionForUser = {} as jest.Mocked<DeleteSessionForUser>
     deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: true })
 

+ 15 - 132
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionController.ts

@@ -1,8 +1,6 @@
-import { ErrorTag } from '@standardnotes/responses'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpDelete,
   httpPost,
@@ -13,152 +11,37 @@ import TYPES from '../../Bootstrap/Types'
 import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
 import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
 import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSessionController } from './HomeServer/HomeServerSessionController'
 
 @controller('/session')
-export class InversifyExpressSessionController extends BaseHttpController {
+export class InversifyExpressSessionController extends HomeServerSessionController {
   constructor(
-    @inject(TYPES.Auth_DeleteSessionForUser) private deleteSessionForUser: DeleteSessionForUser,
+    @inject(TYPES.Auth_DeleteSessionForUser) override deleteSessionForUser: DeleteSessionForUser,
     @inject(TYPES.Auth_DeletePreviousSessionsForUser)
-    private deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
-    @inject(TYPES.Auth_RefreshSessionToken) private refreshSessionToken: RefreshSessionToken,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    override deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
+    @inject(TYPES.Auth_RefreshSessionToken) override refreshSessionToken: RefreshSessionToken,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.sessions.delete', this.deleteSession.bind(this))
-    this.controllerContainer.register('auth.sessions.deleteAll', this.deleteAllSessions.bind(this))
-    this.controllerContainer.register('auth.sessions.refresh', this.refresh.bind(this))
+    super(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
   }
 
   @httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
-  async deleteSession(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
-    if (response.locals.readOnlyAccess) {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.ReadOnlyAccess,
-            message: 'Session has read-only access.',
-          },
-        },
-        401,
-      )
-    }
-
-    if (!request.body.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Please provide the session identifier.',
-          },
-        },
-        400,
-      )
-    }
-
-    if (request.body.uuid === response.locals.session.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'You can not delete your current session.',
-          },
-        },
-        400,
-      )
-    }
-
-    const useCaseResponse = await this.deleteSessionForUser.execute({
-      userUuid: response.locals.user.uuid,
-      sessionUuid: request.body.uuid,
-    })
-
-    if (!useCaseResponse.success) {
-      return this.json(
-        {
-          error: {
-            message: useCaseResponse.errorMessage,
-          },
-        },
-        400,
-      )
-    }
-
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
-
-    return this.statusCode(204)
+  override async deleteSession(
+    request: Request,
+    response: Response,
+  ): Promise<results.JsonResult | results.StatusCodeResult> {
+    return super.deleteSession(request, response)
   }
 
   @httpDelete('/all', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
-  async deleteAllSessions(
+  override async deleteAllSessions(
     _request: Request,
     response: Response,
   ): Promise<results.JsonResult | results.StatusCodeResult> {
-    if (response.locals.readOnlyAccess) {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.ReadOnlyAccess,
-            message: 'Session has read-only access.',
-          },
-        },
-        401,
-      )
-    }
-
-    if (!response.locals.user) {
-      return this.json(
-        {
-          error: {
-            message: 'No session exists with the provided identifier.',
-          },
-        },
-        401,
-      )
-    }
-
-    await this.deletePreviousSessionsForUser.execute({
-      userUuid: response.locals.user.uuid,
-      currentSessionUuid: response.locals.session.uuid,
-    })
-
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
-
-    return this.statusCode(204)
+    return super.deleteAllSessions(_request, response)
   }
 
   @httpPost('/refresh')
-  async refresh(request: Request, response: Response): Promise<results.JsonResult> {
-    if (!request.body.access_token || !request.body.refresh_token) {
-      return this.json(
-        {
-          error: {
-            message: 'Please provide all required parameters.',
-          },
-        },
-        400,
-      )
-    }
-
-    const result = await this.refreshSessionToken.execute({
-      accessToken: request.body.access_token,
-      refreshToken: request.body.refresh_token,
-    })
-
-    if (!result.success) {
-      return this.json(
-        {
-          error: {
-            tag: result.errorTag,
-            message: result.errorMessage,
-          },
-        },
-        400,
-      )
-    }
-
-    response.setHeader('x-invalidate-cache', result.userUuid as string)
-    return this.json({
-      session: result.sessionPayload,
-    })
+  override async refresh(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.refresh(request, response)
   }
 }

+ 0 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionsController.spec.ts

@@ -4,7 +4,6 @@ import * as express from 'express'
 
 import { InversifyExpressSessionsController } from './InversifyExpressSessionsController'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { User } from '@standardnotes/responses'
 
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
@@ -22,7 +21,6 @@ describe('InversifyExpressSessionsController', () => {
   let response: express.Response
   let user: User
   let createCrossServiceToken: CreateCrossServiceToken
-  let controllerContainer: ControllerContainerInterface
 
   const createController = () =>
     new InversifyExpressSessionsController(
@@ -30,13 +28,9 @@ describe('InversifyExpressSessionsController', () => {
       authenticateRequest,
       sessionProjector,
       createCrossServiceToken,
-      controllerContainer,
     )
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     session = {} as jest.Mocked<Session>
 
     user = {} as jest.Mocked<User>

+ 11 - 57
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSessionsController.ts

@@ -1,85 +1,39 @@
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpGet,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
 } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 import TYPES from '../../Bootstrap/Types'
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
 import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { SessionProjector } from '../../Projection/SessionProjector'
-import { User } from '../../Domain/User/User'
 import { Session } from '../../Domain/Session/Session'
+import { HomeServerSessionsController } from './HomeServer/HomeServerSessionsController'
 
 @controller('/sessions')
-export class InversifyExpressSessionsController extends BaseHttpController {
+export class InversifyExpressSessionsController extends HomeServerSessionsController {
   constructor(
-    @inject(TYPES.Auth_GetActiveSessionsForUser) private getActiveSessionsForUser: GetActiveSessionsForUser,
-    @inject(TYPES.Auth_AuthenticateRequest) private authenticateRequest: AuthenticateRequest,
-    @inject(TYPES.Auth_SessionProjector) private sessionProjector: ProjectorInterface<Session>,
-    @inject(TYPES.Auth_CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    @inject(TYPES.Auth_GetActiveSessionsForUser) override getActiveSessionsForUser: GetActiveSessionsForUser,
+    @inject(TYPES.Auth_AuthenticateRequest) override authenticateRequest: AuthenticateRequest,
+    @inject(TYPES.Auth_SessionProjector) override sessionProjector: ProjectorInterface<Session>,
+    @inject(TYPES.Auth_CreateCrossServiceToken) override createCrossServiceToken: CreateCrossServiceToken,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.sessions.list', this.getSessions.bind(this))
-    this.controllerContainer.register('auth.sessions.validate', this.validate.bind(this))
+    super(getActiveSessionsForUser, authenticateRequest, sessionProjector, createCrossServiceToken)
   }
 
   @httpPost('/validate')
-  async validate(request: Request): Promise<results.JsonResult> {
-    const authenticateRequestResponse = await this.authenticateRequest.execute({
-      authorizationHeader: request.headers.authorization,
-    })
-
-    if (!authenticateRequestResponse.success) {
-      return this.json(
-        {
-          error: {
-            tag: authenticateRequestResponse.errorTag,
-            message: authenticateRequestResponse.errorMessage,
-          },
-        },
-        authenticateRequestResponse.responseCode,
-      )
-    }
-
-    const user = authenticateRequestResponse.user as User
-
-    const result = await this.createCrossServiceToken.execute({
-      user,
-      session: authenticateRequestResponse.session,
-    })
-
-    return this.json({ authToken: result.token })
+  override async validate(request: Request): Promise<results.JsonResult> {
+    return super.validate(request)
   }
 
   @httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
-  async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
-    if (response.locals.readOnlyAccess) {
-      return this.json([])
-    }
-
-    const useCaseResponse = await this.getActiveSessionsForUser.execute({
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(
-      useCaseResponse.sessions.map((session) =>
-        this.sessionProjector.projectCustom(
-          SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
-          session,
-          response.locals.session,
-        ),
-      ),
-    )
+  override async getSessions(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSessions(_request, response)
   }
 }

+ 1 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSettingsController.spec.ts

@@ -4,7 +4,6 @@ import * as express from 'express'
 
 import { InversifyExpressSettingsController } from './InversifyExpressSettingsController'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
@@ -21,15 +20,11 @@ describe('InversifyExpressSettingsController', () => {
   let request: express.Request
   let response: express.Response
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
   const createController = () =>
-    new InversifyExpressSettingsController(getSettings, getSetting, updateSetting, deleteSetting, controllerContainer)
+    new InversifyExpressSettingsController(getSettings, getSetting, updateSetting, deleteSetting)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     deleteSetting = {} as jest.Mocked<DeleteSetting>
     deleteSetting.execute = jest.fn().mockReturnValue({ success: true })
 

+ 18 - 132
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSettingsController.ts

@@ -1,8 +1,6 @@
-import { ErrorTag } from '@standardnotes/responses'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpDelete,
   httpGet,
@@ -11,155 +9,43 @@ import {
   results,
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
-import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
 import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSettingsController } from './HomeServer/HomeServerSettingsController'
 
 @controller('/users/:userUuid')
-export class InversifyExpressSettingsController extends BaseHttpController {
+export class InversifyExpressSettingsController extends HomeServerSettingsController {
   constructor(
-    @inject(TYPES.Auth_GetSettings) private doGetSettings: GetSettings,
-    @inject(TYPES.Auth_GetSetting) private doGetSetting: GetSetting,
-    @inject(TYPES.Auth_UpdateSetting) private doUpdateSetting: UpdateSetting,
-    @inject(TYPES.Auth_DeleteSetting) private doDeleteSetting: DeleteSetting,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    @inject(TYPES.Auth_GetSettings) override doGetSettings: GetSettings,
+    @inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting,
+    @inject(TYPES.Auth_UpdateSetting) override doUpdateSetting: UpdateSetting,
+    @inject(TYPES.Auth_DeleteSetting) override doDeleteSetting: DeleteSetting,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.users.getSettings', this.getSettings.bind(this))
-    this.controllerContainer.register('auth.users.getSetting', this.getSetting.bind(this))
-    this.controllerContainer.register('auth.users.updateSetting', this.updateSetting.bind(this))
-    this.controllerContainer.register('auth.users.deleteSetting', this.deleteSetting.bind(this))
+    super(doGetSettings, doGetSetting, doUpdateSetting, doDeleteSetting)
   }
 
   @httpGet('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const { userUuid } = request.params
-    const result = await this.doGetSettings.execute({ userUuid })
-
-    return this.json(result)
+  override async getSettings(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSettings(request, response)
   }
 
   @httpGet('/settings/:settingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const { userUuid, settingName } = request.params
-    const result = await this.doGetSetting.execute({ userUuid, settingName: settingName.toUpperCase() })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async getSetting(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSetting(request, response)
   }
 
   @httpPut('/settings', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async updateSetting(request: Request, response: Response): Promise<results.JsonResult | results.StatusCodeResult> {
-    if (response.locals.readOnlyAccess) {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.ReadOnlyAccess,
-            message: 'Session has read-only access.',
-          },
-        },
-        401,
-      )
-    }
-
-    if (request.params.userUuid !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const { name, value, serverEncryptionVersion = EncryptionVersion.Default, sensitive = false } = request.body
-
-    const props = {
-      name,
-      unencryptedValue: value,
-      serverEncryptionVersion,
-      sensitive,
-    }
-
-    const { userUuid } = request.params
-    const result = await this.doUpdateSetting.execute({
-      userUuid,
-      props,
-    })
-
-    if (result.success) {
-      return this.json({ setting: result.setting }, result.statusCode)
-    }
-
-    return this.json(result, result.statusCode)
+  override async updateSetting(
+    request: Request,
+    response: Response,
+  ): Promise<results.JsonResult | results.StatusCodeResult> {
+    return super.updateSetting(request, response)
   }
 
   @httpDelete('/settings/:settingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async deleteSetting(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.userUuid !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const { userUuid, settingName } = request.params
-
-    const result = await this.doDeleteSetting.execute({
-      userUuid,
-      settingName,
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async deleteSetting(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.deleteSetting(request, response)
   }
 }

+ 16 - 55
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController.ts

@@ -1,8 +1,5 @@
-import { ApiVersion } from '@standardnotes/api'
-import { Role } from '@standardnotes/security'
 import { Request, Response } from 'express'
 import {
-  BaseHttpController,
   controller,
   httpDelete,
   httpGet,
@@ -10,79 +7,43 @@ import {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
 } from 'inversify-express-utils'
+import { inject } from 'inversify'
+
 import TYPES from '../../Bootstrap/Types'
 import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
-import { inject } from 'inversify'
+import { HomeServerSubscriptionInvitesController } from './HomeServer/HomeServerSubscriptionInvitesController'
 
 @controller('/subscription-invites')
-export class InversifyExpressSubscriptionInvitesController extends BaseHttpController {
+export class InversifyExpressSubscriptionInvitesController extends HomeServerSubscriptionInvitesController {
   constructor(
     @inject(TYPES.Auth_SubscriptionInvitesController)
-    private subscriptionInvitesController: SubscriptionInvitesController,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    override subscriptionInvitesController: SubscriptionInvitesController,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.subscriptionInvites.accept', this.acceptInvite.bind(this))
-    this.controllerContainer.register('auth.subscriptionInvites.declineInvite', this.declineInvite.bind(this))
-    this.controllerContainer.register('auth.subscriptionInvites.create', this.inviteToSubscriptionSharing.bind(this))
-    this.controllerContainer.register('auth.subscriptionInvites.delete', this.cancelSubscriptionSharing.bind(this))
-    this.controllerContainer.register('auth.subscriptionInvites.list', this.listInvites.bind(this))
+    super(subscriptionInvitesController)
   }
 
   @httpPost('/:inviteUuid/accept', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
-    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)
-
-    return this.json(result.data, result.status)
+  override async acceptInvite(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.acceptInvite(request, response)
   }
 
   @httpGet('/:inviteUuid/decline')
-  async declineInvite(request: Request): Promise<results.JsonResult> {
-    const response = await this.subscriptionInvitesController.declineInvite({
-      api: request.query.api as ApiVersion,
-      inviteUuid: request.params.inviteUuid,
-    })
-
-    return this.json(response.data, response.status)
+  override async declineInvite(request: Request): Promise<results.JsonResult> {
+    return super.declineInvite(request)
   }
 
   @httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
-    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),
-    })
-
-    return this.json(result.data, result.status)
+  override async inviteToSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.inviteToSubscriptionSharing(request, response)
   }
 
   @httpDelete('/:inviteUuid', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.subscriptionInvitesController.cancelInvite({
-      ...request.body,
-      inviteUuid: request.params.inviteUuid,
-      inviterEmail: response.locals.user.email,
-    })
-
-    return this.json(result.data, result.status)
+  override async cancelSubscriptionSharing(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.cancelSubscriptionSharing(request, response)
   }
 
   @httpGet('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.subscriptionInvitesController.listInvites({
-      ...request.body,
-      inviterEmail: response.locals.user.email,
-    })
-
-    return this.json(result.data, result.status)
+  override async listInvites(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.listInvites(request, response)
   }
 }

+ 1 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController.spec.ts

@@ -4,7 +4,6 @@ import * as express from 'express'
 
 import { results } from 'inversify-express-utils'
 import { InversifyExpressSubscriptionSettingsController } from './InversifyExpressSubscriptionSettingsController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { User } from '../../Domain/User/User'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 
@@ -14,14 +13,10 @@ describe('InversifyExpressSubscriptionSettingsController', () => {
   let request: express.Request
   let response: express.Response
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
-  const createController = () => new InversifyExpressSubscriptionSettingsController(getSetting, controllerContainer)
+  const createController = () => new InversifyExpressSubscriptionSettingsController(getSetting)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user.uuid = '123'
 

+ 6 - 21
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionSettingsController.ts

@@ -1,7 +1,6 @@
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpGet,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9,30 +8,16 @@ import {
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSubscriptionSettingsController } from './HomeServer/HomeServerSubscriptionSettingsController'
 
 @controller('/users/:userUuid')
-export class InversifyExpressSubscriptionSettingsController extends BaseHttpController {
-  constructor(
-    @inject(TYPES.Auth_GetSetting) private doGetSetting: GetSetting,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
-  ) {
-    super()
-
-    this.controllerContainer.register('auth.users.getSubscriptionSetting', this.getSubscriptionSetting.bind(this))
+export class InversifyExpressSubscriptionSettingsController extends HomeServerSubscriptionSettingsController {
+  constructor(@inject(TYPES.Auth_GetSetting) override doGetSetting: GetSetting) {
+    super(doGetSetting)
   }
 
   @httpGet('/subscription-settings/:subscriptionSettingName', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.doGetSetting.execute({
-      userUuid: response.locals.user.uuid,
-      settingName: request.params.subscriptionSettingName.toUpperCase(),
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async getSubscriptionSetting(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSubscriptionSetting(request, response)
   }
 }

+ 1 - 7
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionTokensController.spec.ts

@@ -5,7 +5,7 @@ import { results } from 'inversify-express-utils'
 
 import { InversifyExpressSubscriptionTokensController } from './InversifyExpressSubscriptionTokensController'
 import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+
 import { Setting } from '../../Domain/Setting/Setting'
 import { SettingServiceInterface } from '../../Domain/Setting/SettingServiceInterface'
 import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken'
@@ -30,8 +30,6 @@ describe('InversifyExpressSubscriptionTokensController', () => {
   let user: User
   let role: Role
 
-  let controllerContainer: ControllerContainerInterface
-
   const createController = () =>
     new InversifyExpressSubscriptionTokensController(
       createSubscriptionToken,
@@ -41,13 +39,9 @@ describe('InversifyExpressSubscriptionTokensController', () => {
       roleProjector,
       tokenEncoder,
       jwtTTL,
-      controllerContainer,
     )
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user.uuid = '123'
     user.roles = Promise.resolve([role])

+ 22 - 87
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionTokensController.ts

@@ -1,10 +1,7 @@
 import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
-import { ErrorTag } from '@standardnotes/responses'
-import { SettingName } from '@standardnotes/settings'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import {
-  BaseHttpController,
   controller,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -18,99 +15,37 @@ import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/Authenticate
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { User } from '../../Domain/User/User'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSubscriptionTokensController } from './HomeServer/HomeServerSubscriptionTokensController'
 
 @controller('/subscription-tokens')
-export class InversifyExpressSubscriptionTokensController extends BaseHttpController {
+export class InversifyExpressSubscriptionTokensController extends HomeServerSubscriptionTokensController {
   constructor(
-    @inject(TYPES.Auth_CreateSubscriptionToken) private createSubscriptionToken: CreateSubscriptionToken,
-    @inject(TYPES.Auth_AuthenticateSubscriptionToken) private authenticateToken: AuthenticateSubscriptionToken,
-    @inject(TYPES.Auth_SettingService) private settingService: SettingServiceInterface,
-    @inject(TYPES.Auth_UserProjector) private userProjector: ProjectorInterface<User>,
-    @inject(TYPES.Auth_RoleProjector) private roleProjector: ProjectorInterface<Role>,
-    @inject(TYPES.Auth_CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
-    @inject(TYPES.Auth_AUTH_JWT_TTL) private jwtTTL: number,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    @inject(TYPES.Auth_CreateSubscriptionToken) override createSubscriptionToken: CreateSubscriptionToken,
+    @inject(TYPES.Auth_AuthenticateSubscriptionToken) override authenticateToken: AuthenticateSubscriptionToken,
+    @inject(TYPES.Auth_SettingService) override settingService: SettingServiceInterface,
+    @inject(TYPES.Auth_UserProjector) override userProjector: ProjectorInterface<User>,
+    @inject(TYPES.Auth_RoleProjector) override roleProjector: ProjectorInterface<Role>,
+    @inject(TYPES.Auth_CrossServiceTokenEncoder) override tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
+    @inject(TYPES.Auth_AUTH_JWT_TTL) override jwtTTL: number,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.subscription-tokens.create', this.createToken.bind(this))
+    super(
+      createSubscriptionToken,
+      authenticateToken,
+      settingService,
+      userProjector,
+      roleProjector,
+      tokenEncoder,
+      jwtTTL,
+    )
   }
 
   @httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async createToken(_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,
-      )
-    }
-
-    const result = await this.createSubscriptionToken.execute({
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json({
-      token: result.subscriptionToken.token,
-    })
+  override async createToken(_request: Request, response: Response): Promise<results.JsonResult> {
+    return super.createToken(_request, response)
   }
 
   @httpPost('/:token/validate')
-  async validate(request: Request): Promise<results.JsonResult> {
-    const authenticateTokenResponse = await this.authenticateToken.execute({
-      token: request.params.token,
-    })
-
-    if (!authenticateTokenResponse.success) {
-      return this.json(
-        {
-          error: {
-            tag: 'invalid-auth',
-            message: 'Invalid login credentials.',
-          },
-        },
-        401,
-      )
-    }
-
-    const user = authenticateTokenResponse.user as User
-    let extensionKey = undefined
-    const extensionKeySetting = await this.settingService.findSettingWithDecryptedValue({
-      settingName: SettingName.create(SettingName.NAMES.ExtensionKey).getValue(),
-      userUuid: user.uuid,
-    })
-    if (extensionKeySetting !== null) {
-      extensionKey = extensionKeySetting.value as string
-    }
-
-    const roles = await user.roles
-
-    const authTokenData: CrossServiceTokenData = {
-      user: await this.projectUser(user),
-      roles: await this.projectRoles(roles),
-      extensionKey,
-    }
-
-    const authToken = this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL)
-
-    return this.json({ authToken })
-  }
-
-  private async projectUser(user: User): Promise<{ uuid: string; email: string }> {
-    return <{ uuid: string; email: string }>await this.userProjector.projectSimple(user)
-  }
-
-  private async projectRoles(roles: Array<Role>): Promise<Array<{ uuid: string; name: string }>> {
-    const roleProjections = []
-    for (const role of roles) {
-      roleProjections.push(<{ uuid: string; name: string }>await this.roleProjector.projectSimple(role))
-    }
-
-    return roleProjections
+  override async validate(request: Request): Promise<results.JsonResult> {
+    return super.validate(request)
   }
 }

+ 7 - 18
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController.ts

@@ -1,30 +1,19 @@
 import { Request, Response } from 'express'
-import { BaseHttpController, results, httpPost, controller } from 'inversify-express-utils'
+import { results, httpPost, controller } from 'inversify-express-utils'
 
 import TYPES from '../../Bootstrap/Types'
 import { UserRequestsController } from '../../Controller/UserRequestsController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
+import { HomeServerUserRequestsController } from './HomeServer/HomeServerUserRequestsController'
 
 @controller('/users/:userUuid/requests')
-export class InversifyExpressUserRequestsController extends BaseHttpController {
-  constructor(
-    @inject(TYPES.Auth_UserRequestsController) private userRequestsController: UserRequestsController,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
-  ) {
-    super()
-
-    this.controllerContainer.register('auth.users.createRequest', this.submitRequest.bind(this))
+export class InversifyExpressUserRequestsController extends HomeServerUserRequestsController {
+  constructor(@inject(TYPES.Auth_UserRequestsController) override userRequestsController: UserRequestsController) {
+    super(userRequestsController)
   }
 
   @httpPost('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.userRequestsController.submitUserRequest({
-      requestType: request.body.requestType,
-      userUuid: response.locals.user.uuid,
-      userEmail: response.locals.user.email,
-    })
-
-    return this.json(result.data, result.status)
+  override async submitRequest(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.submitRequest(request, response)
   }
 }

+ 1 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.spec.ts

@@ -4,7 +4,7 @@ import * as express from 'express'
 
 import { InversifyExpressUsersController } from './InversifyExpressUsersController'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
+import { Username } from '@standardnotes/domain-core'
 import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
 import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
 import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
@@ -28,7 +28,6 @@ describe('InversifyExpressUsersController', () => {
   let request: express.Request
   let response: express.Response
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
   const createController = () =>
     new InversifyExpressUsersController(
@@ -39,13 +38,9 @@ describe('InversifyExpressUsersController', () => {
       clearLoginAttempts,
       increaseLoginAttempts,
       changeCredentials,
-      controllerContainer,
     )
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     updateUser = {} as jest.Mocked<UpdateUser>
     updateUser.execute = jest.fn()
 

+ 28 - 210
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressUsersController.ts

@@ -1,8 +1,6 @@
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
-import { ErrorTag } from '@standardnotes/responses'
 import {
-  BaseHttpController,
   controller,
   httpDelete,
   httpGet,
@@ -19,232 +17,52 @@ import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/Ge
 import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
 import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
 import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
-import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
+import { HomeServerUsersController } from './HomeServer/HomeServerUsersController'
 
 @controller('/users')
-export class InversifyExpressUsersController extends BaseHttpController {
+export class InversifyExpressUsersController extends HomeServerUsersController {
   constructor(
-    @inject(TYPES.Auth_UpdateUser) private updateUser: UpdateUser,
-    @inject(TYPES.Auth_GetUserKeyParams) private getUserKeyParams: GetUserKeyParams,
-    @inject(TYPES.Auth_DeleteAccount) private doDeleteAccount: DeleteAccount,
-    @inject(TYPES.Auth_GetUserSubscription) private doGetUserSubscription: GetUserSubscription,
-    @inject(TYPES.Auth_ClearLoginAttempts) private clearLoginAttempts: ClearLoginAttempts,
-    @inject(TYPES.Auth_IncreaseLoginAttempts) private increaseLoginAttempts: IncreaseLoginAttempts,
-    @inject(TYPES.Auth_ChangeCredentials) private changeCredentialsUseCase: ChangeCredentials,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    @inject(TYPES.Auth_UpdateUser) override updateUser: UpdateUser,
+    @inject(TYPES.Auth_GetUserKeyParams) override getUserKeyParams: GetUserKeyParams,
+    @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()
-
-    this.controllerContainer.register('auth.users.update', this.update.bind(this))
-    this.controllerContainer.register('auth.users.getKeyParams', this.keyParams.bind(this))
-    this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
-    this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
+    super(
+      updateUser,
+      getUserKeyParams,
+      doDeleteAccount,
+      doGetUserSubscription,
+      clearLoginAttempts,
+      increaseLoginAttempts,
+      changeCredentialsUseCase,
+    )
   }
 
   @httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  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,
-      pwFunc: request.body.pw_func,
-      pwAlg: request.body.pw_alg,
-      pwCost: request.body.pw_cost,
-      pwKeySize: request.body.pw_key_size,
-      pwNonce: request.body.pw_nonce,
-      pwSalt: request.body.pw_salt,
-      kpOrigination: request.body.origination,
-      kpCreated: request.body.created,
-      version: request.body.version,
-    })
-
-    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,
-    )
+  override async update(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.update(request, response)
   }
 
   @httpGet('/params')
-  async keyParams(request: Request): Promise<results.JsonResult> {
-    const email = 'email' in request.query ? <string>request.query.email : undefined
-    const userUuid = 'uuid' in request.query ? <string>request.query.uuid : undefined
-
-    if (!email && !userUuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Missing mandatory request query parameters.',
-          },
-        },
-        400,
-      )
-    }
-
-    const result = await this.getUserKeyParams.execute({
-      email,
-      userUuid,
-      authenticated: request.query.authenticated === 'true',
-    })
-
-    return this.json(result.keyParams)
+  override async keyParams(request: Request): Promise<results.JsonResult> {
+    return super.keyParams(request)
   }
 
   @httpDelete('/:email')
-  async deleteAccount(request: Request): Promise<results.JsonResult> {
-    const result = await this.doDeleteAccount.execute({
-      email: request.params.email,
-    })
-
-    return this.json({ message: result.message }, result.responseCode)
+  override async deleteAccount(request: Request): Promise<results.JsonResult> {
+    return super.deleteAccount(request)
   }
 
   @httpGet('/:userUuid/subscription', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
-    if (request.params.userUuid !== response.locals.user.uuid) {
-      return this.json(
-        {
-          error: {
-            message: 'Operation not allowed.',
-          },
-        },
-        401,
-      )
-    }
-
-    const result = await this.doGetUserSubscription.execute({
-      userUuid: request.params.userUuid,
-    })
-
-    if (result.success) {
-      return this.json(result)
-    }
-
-    return this.json(result, 400)
+  override async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.getSubscription(request, response)
   }
 
   @httpPut('/:userId/attributes/credentials', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-  async changeCredentials(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.body.current_password) {
-      return this.json(
-        {
-          error: {
-            message:
-              'Your current password is required to change your password. Please update your application if you do not see this option.',
-          },
-        },
-        400,
-      )
-    }
-
-    if (!request.body.new_password) {
-      return this.json(
-        {
-          error: {
-            message: 'Your new password is required to change your password. Please try again.',
-          },
-        },
-        400,
-      )
-    }
-
-    if (!request.body.pw_nonce) {
-      return this.json(
-        {
-          error: {
-            message: 'The change password request is missing new auth parameters. Please try again.',
-          },
-        },
-        400,
-      )
-    }
-    const usernameOrError = Username.create(response.locals.user.email)
-    if (usernameOrError.isFailed()) {
-      return this.json(
-        {
-          error: {
-            message: 'Invalid username.',
-          },
-        },
-        400,
-      )
-    }
-    const username = usernameOrError.getValue()
-
-    const changeCredentialsResult = await this.changeCredentialsUseCase.execute({
-      username,
-      apiVersion: request.body.api,
-      currentPassword: request.body.current_password,
-      newPassword: request.body.new_password,
-      newEmail: request.body.new_email,
-      pwNonce: request.body.pw_nonce,
-      kpCreated: request.body.created,
-      kpOrigination: request.body.origination,
-      updatedWithUserAgent: <string>request.headers['user-agent'],
-      protocolVersion: request.body.version,
-    })
-
-    if (!changeCredentialsResult.success) {
-      await this.increaseLoginAttempts.execute({ email: response.locals.user.email })
-
-      return this.json(
-        {
-          error: {
-            message: changeCredentialsResult.errorMessage,
-          },
-        },
-        401,
-      )
-    }
-
-    await this.clearLoginAttempts.execute({ email: response.locals.user.email })
-
-    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
-
-    return this.json(changeCredentialsResult.authResponse)
+  override async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.changeCredentials(request, response)
   }
 }

+ 1 - 6
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressValetTokenController.spec.ts

@@ -4,20 +4,15 @@ import { Request, Response } from 'express'
 import { results } from 'inversify-express-utils'
 import { InversifyExpressValetTokenController } from './InversifyExpressValetTokenController'
 import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 describe('InversifyExpressValetTokenController', () => {
   let createValetToken: CreateValetToken
   let request: Request
   let response: Response
-  let controllerContainer: ControllerContainerInterface
 
-  const createController = () => new InversifyExpressValetTokenController(createValetToken, controllerContainer)
+  const createController = () => new InversifyExpressValetTokenController(createValetToken)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     createValetToken = {} as jest.Mocked<CreateValetToken>
     createValetToken.execute = jest.fn().mockReturnValue({ success: true, valetToken: 'foobar' })
 

+ 6 - 53
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressValetTokenController.ts

@@ -1,70 +1,23 @@
 import { inject } from 'inversify'
 import { Request, Response } from 'express'
 import {
-  BaseHttpController,
   controller,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
 } from 'inversify-express-utils'
-import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
-import { ValetTokenOperation } from '@standardnotes/security'
-import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
 import TYPES from '../../Bootstrap/Types'
 import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
+import { HomeServerValetTokenController } from './HomeServer/HomeServerValetTokenController'
 
 @controller('/valet-tokens', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
-export class InversifyExpressValetTokenController extends BaseHttpController {
-  constructor(
-    @inject(TYPES.Auth_CreateValetToken) private createValetKey: CreateValetToken,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
-  ) {
-    super()
-
-    this.controllerContainer.register('auth.valet-tokens.create', this.create.bind(this))
+export class InversifyExpressValetTokenController extends HomeServerValetTokenController {
+  constructor(@inject(TYPES.Auth_CreateValetToken) override createValetKey: CreateValetToken) {
+    super(createValetKey)
   }
 
   @httpPost('/')
-  public async create(request: Request, response: Response): Promise<results.JsonResult> {
-    const payload: CreateValetTokenPayload = request.body
-
-    if (response.locals.readOnlyAccess && payload.operation !== 'read') {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.ReadOnlyAccess,
-            message: 'Session has read-only access.',
-          },
-        },
-        401,
-      )
-    }
-
-    for (const resource of payload.resources) {
-      const resourceUuidOrError = Uuid.create(resource.remoteIdentifier)
-      if (resourceUuidOrError.isFailed()) {
-        return this.json(
-          {
-            error: {
-              tag: ErrorTag.ParametersInvalid,
-              message: 'Invalid remote resource identifier.',
-            },
-          },
-          400,
-        )
-      }
-    }
-
-    const createValetKeyResponse = await this.createValetKey.execute({
-      userUuid: response.locals.user.uuid,
-      operation: payload.operation as ValetTokenOperation,
-      resources: payload.resources,
-    })
-
-    if (!createValetKeyResponse.success) {
-      return this.json(createValetKeyResponse, 403)
-    }
-
-    return this.json(createValetKeyResponse)
+  override async create(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.create(request, response)
   }
 }

+ 7 - 42
packages/auth/src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController.ts

@@ -1,63 +1,28 @@
-import { ErrorTag } from '@standardnotes/responses'
 import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
 import { Request } from 'express'
 import {
-  BaseHttpController,
   controller,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
 } from 'inversify-express-utils'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
 import TYPES from '../../Bootstrap/Types'
+import { HomeServerWebSocketsController } from './HomeServer/HomeServerWebSocketsController'
 
 @controller('/sockets')
-export class InversifyExpressWebSocketsController extends BaseHttpController {
+export class InversifyExpressWebSocketsController extends HomeServerWebSocketsController {
   constructor(
-    @inject(TYPES.Auth_CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
+    @inject(TYPES.Auth_CreateCrossServiceToken) override createCrossServiceToken: CreateCrossServiceToken,
     @inject(TYPES.Auth_WebSocketConnectionTokenDecoder)
-    private tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
-    @inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    override tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
   ) {
-    super()
-
-    this.controllerContainer.register('auth.webSockets.validateToken', this.validateToken.bind(this))
+    super(createCrossServiceToken, tokenDecoder)
   }
 
   @httpPost('/tokens/validate')
-  async validateToken(request: Request): Promise<results.JsonResult> {
-    if (!request.headers.authorization) {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.AuthInvalid,
-            message: 'Invalid authorization token.',
-          },
-        },
-        401,
-      )
-    }
-
-    const token: WebSocketConnectionTokenData | undefined = this.tokenDecoder.decodeToken(request.headers.authorization)
-
-    if (token === undefined) {
-      return this.json(
-        {
-          error: {
-            tag: ErrorTag.AuthInvalid,
-            message: 'Invalid authorization token.',
-          },
-        },
-        401,
-      )
-    }
-
-    const result = await this.createCrossServiceToken.execute({
-      userUuid: token.userUuid,
-    })
-
-    return this.json({ authToken: result.token })
+  override async validateToken(request: Request): Promise<results.JsonResult> {
+    return super.validateToken(request)
   }
 }

+ 1 - 0
packages/domain-core/src/Domain/Service/ServiceIdentifier.ts

@@ -8,6 +8,7 @@ export class ServiceIdentifier extends ValueObject<ServiceIdentifierProps> {
     Auth: 'Auth',
     SyncingServer: 'SyncingServer',
     Revisions: 'Revisions',
+    Files: 'Files',
   }
 
   get value(): string {

+ 4 - 4
packages/files/bin/server.ts

@@ -4,8 +4,8 @@ import 'newrelic'
 
 import * as busboy from 'connect-busboy'
 
-import '../src/Controller/HealthCheckController'
-import '../src/Controller/FilesController'
+import '../src/Infra/InversifyExpress/InversifyExpressHealthCheckController'
+import '../src/Infra/InversifyExpress/InversifyExpressFilesController'
 
 import helmet from 'helmet'
 import * as cors from 'cors'
@@ -28,7 +28,7 @@ void container.load().then((container) => {
 
   server.setConfig((app) => {
     app.use((_request: Request, response: Response, next: NextFunction) => {
-      response.setHeader('X-Files-Version', container.get(TYPES.VERSION))
+      response.setHeader('X-Files-Version', container.get(TYPES.Files_VERSION))
       next()
     })
     app.use(
@@ -74,7 +74,7 @@ void container.load().then((container) => {
     )
   })
 
-  const logger: winston.Logger = container.get(TYPES.Logger)
+  const logger: winston.Logger = container.get(TYPES.Files_Logger)
 
   server.setErrorConfig((app) => {
     app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {

+ 4 - 2
packages/files/bin/worker.ts

@@ -18,11 +18,13 @@ void container.load().then((container) => {
   const env: Env = new Env()
   env.load()
 
-  const logger: Logger = container.get(TYPES.Logger)
+  const logger: Logger = container.get(TYPES.Files_Logger)
 
   logger.info('Starting worker...')
 
-  const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
+  const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(
+    TYPES.Files_DomainEventSubscriberFactory,
+  )
   subscriberFactory.create().start()
 
   setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)

+ 1 - 0
packages/files/package.json

@@ -18,6 +18,7 @@
     "setup:env": "cp .env.sample .env",
     "build": "tsc --build",
     "lint": "eslint . --ext .ts",
+    "lint:fix": "eslint . --fix --ext .ts",
     "pretest": "yarn lint && yarn build",
     "test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
     "start": "yarn node dist/bin/server.js",

+ 138 - 108
packages/files/src/Bootstrap/Container.ts

@@ -8,12 +8,14 @@ import { Container } from 'inversify'
 import { Env } from './Env'
 import TYPES from './Types'
 import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
-import { ValetTokenAuthMiddleware } from '../Controller/ValetTokenAuthMiddleware'
+import { ValetTokenAuthMiddleware } from '../Infra/InversifyExpress/Middleware/ValetTokenAuthMiddleware'
 import { TokenDecoder, TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
 import { Timer, TimerInterface } from '@standardnotes/time'
 import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
 import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
 import {
+  DirectCallDomainEventPublisher,
+  DirectCallEventMessageHandler,
   SNSDomainEventPublisher,
   SQSDomainEventSubscriberFactory,
   SQSEventMessageHandler,
@@ -38,6 +40,7 @@ import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
 import {
   DomainEventHandlerInterface,
   DomainEventMessageHandlerInterface,
+  DomainEventPublisherInterface,
   DomainEventSubscriberFactoryInterface,
 } from '@standardnotes/domain-events'
 import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
@@ -46,7 +49,10 @@ import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Hand
 import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
 
 export class ContainerConfigLoader {
-  async load(): Promise<Container> {
+  async load(configuration?: { directCallDomainEventPublisher?: DirectCallDomainEventPublisher }): Promise<Container> {
+    const directCallDomainEventPublisher =
+      configuration?.directCallDomainEventPublisher ?? new DirectCallDomainEventPublisher()
+
     const env: Env = new Env()
     env.load()
 
@@ -55,32 +61,88 @@ export class ContainerConfigLoader {
     const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
 
     const logger = this.createLogger({ env })
-    container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
+    container.bind<winston.Logger>(TYPES.Files_Logger).toConstantValue(logger)
 
-    // env vars
-    container.bind(TYPES.S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
-    container.bind(TYPES.S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
-    container.bind(TYPES.VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
-    container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
-    container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
-    container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
-    container.bind(TYPES.MAX_CHUNK_BYTES).toConstantValue(+env.get('MAX_CHUNK_BYTES'))
-    container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
-    container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
-    container
-      .bind(TYPES.FILE_UPLOAD_PATH)
-      .toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
+    container.bind<TimerInterface>(TYPES.Files_Timer).toConstantValue(new Timer())
 
-    const redisUrl = container.get(TYPES.REDIS_URL) as string
-    const isRedisInClusterMode = redisUrl.indexOf(',') > 0
-    let redis
-    if (isRedisInClusterMode) {
-      redis = new Redis.Cluster(redisUrl.split(','))
+    if (isConfiguredForHomeServer) {
+      container
+        .bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository)
+        .toConstantValue(new InMemoryUploadRepository(container.get(TYPES.Files_Timer)))
+
+      container
+        .bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
+        .toConstantValue(directCallDomainEventPublisher)
     } else {
-      redis = new Redis(redisUrl)
+      container.bind(TYPES.Files_S3_BUCKET_NAME).toConstantValue(env.get('S3_BUCKET_NAME', true))
+      container.bind(TYPES.Files_S3_AWS_REGION).toConstantValue(env.get('S3_AWS_REGION', true))
+      container.bind(TYPES.Files_SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN'))
+      container.bind(TYPES.Files_SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
+      container.bind(TYPES.Files_SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL'))
+      container.bind(TYPES.Files_REDIS_URL).toConstantValue(env.get('REDIS_URL'))
+
+      const redisUrl = container.get(TYPES.Files_REDIS_URL) as string
+      const isRedisInClusterMode = redisUrl.indexOf(',') > 0
+      let redis
+      if (isRedisInClusterMode) {
+        redis = new Redis.Cluster(redisUrl.split(','))
+      } else {
+        redis = new Redis(redisUrl)
+      }
+
+      container.bind(TYPES.Files_Redis).toConstantValue(redis)
+
+      if (env.get('SNS_TOPIC_ARN', true)) {
+        const snsConfig: SNSClientConfig = {
+          apiVersion: 'latest',
+          region: env.get('SNS_AWS_REGION', true),
+        }
+        if (env.get('SNS_ENDPOINT', true)) {
+          snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
+        }
+        if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
+          snsConfig.credentials = {
+            accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
+            secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
+          }
+        }
+        container.bind<SNSClient>(TYPES.Files_SNS).toConstantValue(new SNSClient(snsConfig))
+      }
+
+      if (env.get('SQS_QUEUE_URL', true)) {
+        const sqsConfig: SQSClientConfig = {
+          region: env.get('SQS_AWS_REGION', true),
+        }
+        if (env.get('SQS_ENDPOINT', true)) {
+          sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
+        }
+        if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
+          sqsConfig.credentials = {
+            accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
+            secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
+          }
+        }
+        container.bind<SQSClient>(TYPES.Files_SQS).toConstantValue(new SQSClient(sqsConfig))
+      }
+
+      container.bind<UploadRepositoryInterface>(TYPES.Files_UploadRepository).to(RedisUploadRepository)
+
+      container
+        .bind<DomainEventPublisherInterface>(TYPES.Files_DomainEventPublisher)
+        .toConstantValue(
+          new SNSDomainEventPublisher(container.get(TYPES.Files_SNS), container.get(TYPES.Files_SNS_TOPIC_ARN)),
+        )
     }
 
-    container.bind(TYPES.Redis).toConstantValue(redis)
+    // env vars
+    container.bind(TYPES.Files_VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET'))
+    container
+      .bind(TYPES.Files_MAX_CHUNK_BYTES)
+      .toConstantValue(env.get('MAX_CHUNK_BYTES', true) ? +env.get('MAX_CHUNK_BYTES', true) : 100000000)
+    container.bind(TYPES.Files_VERSION).toConstantValue(env.get('VERSION', true) ?? 'development')
+    container
+      .bind(TYPES.Files_FILE_UPLOAD_PATH)
+      .toConstantValue(env.get('FILE_UPLOAD_PATH', true) ?? `${__dirname}/../../uploads`)
 
     if (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true)) {
       const s3Opts: S3ClientConfig = {
@@ -93,115 +155,83 @@ export class ContainerConfigLoader {
         s3Opts.endpoint = env.get('S3_ENDPOINT', true)
       }
       const s3Client = new S3Client(s3Opts)
-      container.bind<S3Client>(TYPES.S3).toConstantValue(s3Client)
-      container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(S3FileDownloader)
-      container.bind<FileUploaderInterface>(TYPES.FileUploader).to(S3FileUploader)
-      container.bind<FileRemoverInterface>(TYPES.FileRemover).to(S3FileRemover)
+      container.bind<S3Client>(TYPES.Files_S3).toConstantValue(s3Client)
+      container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(S3FileDownloader)
+      container.bind<FileUploaderInterface>(TYPES.Files_FileUploader).to(S3FileUploader)
+      container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(S3FileRemover)
     } else {
-      container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
+      container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(FSFileDownloader)
       container
-        .bind<FileUploaderInterface>(TYPES.FileUploader)
-        .toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
-      container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
-    }
-
-    if (env.get('SNS_TOPIC_ARN', true)) {
-      const snsConfig: SNSClientConfig = {
-        apiVersion: 'latest',
-        region: env.get('SNS_AWS_REGION', true),
-      }
-      if (env.get('SNS_ENDPOINT', true)) {
-        snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
-      }
-      if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
-        snsConfig.credentials = {
-          accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
-          secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
-        }
-      }
-      container.bind<SNSClient>(TYPES.SNS).toConstantValue(new SNSClient(snsConfig))
-    }
-
-    if (env.get('SQS_QUEUE_URL', true)) {
-      const sqsConfig: SQSClientConfig = {
-        region: env.get('SQS_AWS_REGION', true),
-      }
-      if (env.get('SQS_ENDPOINT', true)) {
-        sqsConfig.endpoint = env.get('SQS_ENDPOINT', true)
-      }
-      if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
-        sqsConfig.credentials = {
-          accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
-          secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
-        }
-      }
-      container.bind<SQSClient>(TYPES.SQS).toConstantValue(new SQSClient(sqsConfig))
+        .bind<FileUploaderInterface>(TYPES.Files_FileUploader)
+        .toConstantValue(
+          new FSFileUploader(container.get(TYPES.Files_FILE_UPLOAD_PATH), container.get(TYPES.Files_Logger)),
+        )
+      container.bind<FileRemoverInterface>(TYPES.Files_FileRemover).to(FSFileRemover)
     }
 
     // use cases
-    container.bind<UploadFileChunk>(TYPES.UploadFileChunk).to(UploadFileChunk)
-    container.bind<StreamDownloadFile>(TYPES.StreamDownloadFile).to(StreamDownloadFile)
-    container.bind<CreateUploadSession>(TYPES.CreateUploadSession).to(CreateUploadSession)
-    container.bind<FinishUploadSession>(TYPES.FinishUploadSession).to(FinishUploadSession)
-    container.bind<GetFileMetadata>(TYPES.GetFileMetadata).to(GetFileMetadata)
-    container.bind<RemoveFile>(TYPES.RemoveFile).to(RemoveFile)
-    container.bind<MarkFilesToBeRemoved>(TYPES.MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
+    container.bind<UploadFileChunk>(TYPES.Files_UploadFileChunk).to(UploadFileChunk)
+    container.bind<StreamDownloadFile>(TYPES.Files_StreamDownloadFile).to(StreamDownloadFile)
+    container.bind<CreateUploadSession>(TYPES.Files_CreateUploadSession).to(CreateUploadSession)
+    container.bind<FinishUploadSession>(TYPES.Files_FinishUploadSession).to(FinishUploadSession)
+    container.bind<GetFileMetadata>(TYPES.Files_GetFileMetadata).to(GetFileMetadata)
+    container.bind<RemoveFile>(TYPES.Files_RemoveFile).to(RemoveFile)
+    container.bind<MarkFilesToBeRemoved>(TYPES.Files_MarkFilesToBeRemoved).to(MarkFilesToBeRemoved)
 
     // middleware
-    container.bind<ValetTokenAuthMiddleware>(TYPES.ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
+    container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
 
     // services
     container
-      .bind<TokenDecoderInterface<ValetTokenData>>(TYPES.ValetTokenDecoder)
-      .toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
-    container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
-    container.bind<DomainEventFactoryInterface>(TYPES.DomainEventFactory).to(DomainEventFactory)
-
-    // repositories
-    if (isConfiguredForHomeServer) {
-      container
-        .bind<UploadRepositoryInterface>(TYPES.UploadRepository)
-        .toConstantValue(new InMemoryUploadRepository(container.get(TYPES.Timer)))
-    } else {
-      container.bind<UploadRepositoryInterface>(TYPES.UploadRepository).to(RedisUploadRepository)
-    }
-
-    container
-      .bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
-      .toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
+      .bind<TokenDecoderInterface<ValetTokenData>>(TYPES.Files_ValetTokenDecoder)
+      .toConstantValue(new TokenDecoder<ValetTokenData>(container.get(TYPES.Files_VALET_TOKEN_SECRET)))
+    container.bind<DomainEventFactoryInterface>(TYPES.Files_DomainEventFactory).to(DomainEventFactory)
 
     // Handlers
     container
-      .bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
+      .bind<AccountDeletionRequestedEventHandler>(TYPES.Files_AccountDeletionRequestedEventHandler)
       .to(AccountDeletionRequestedEventHandler)
     container
-      .bind<SharedSubscriptionInvitationCanceledEventHandler>(TYPES.SharedSubscriptionInvitationCanceledEventHandler)
+      .bind<SharedSubscriptionInvitationCanceledEventHandler>(
+        TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler,
+      )
       .to(SharedSubscriptionInvitationCanceledEventHandler)
 
     const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
-      ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
+      ['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.Files_AccountDeletionRequestedEventHandler)],
       [
         'SHARED_SUBSCRIPTION_INVITATION_CANCELED',
-        container.get(TYPES.SharedSubscriptionInvitationCanceledEventHandler),
+        container.get(TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler),
       ],
     ])
 
-    container
-      .bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
-      .toConstantValue(
-        env.get('NEW_RELIC_ENABLED', true) === 'true'
-          ? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Logger))
-          : new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Logger)),
-      )
-    container
-      .bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
-      .toConstantValue(
-        new SQSDomainEventSubscriberFactory(
-          container.get(TYPES.SQS),
-          container.get(TYPES.SQS_QUEUE_URL),
-          container.get(TYPES.DomainEventMessageHandler),
-        ),
+    if (isConfiguredForHomeServer) {
+      const directCallEventMessageHandler = new DirectCallEventMessageHandler(
+        eventHandlers,
+        container.get(TYPES.Files_Logger),
       )
+      directCallDomainEventPublisher.register(directCallEventMessageHandler)
+      container
+        .bind<DomainEventMessageHandlerInterface>(TYPES.Files_DomainEventMessageHandler)
+        .toConstantValue(directCallEventMessageHandler)
+    } else {
+      container
+        .bind<DomainEventMessageHandlerInterface>(TYPES.Files_DomainEventMessageHandler)
+        .toConstantValue(
+          env.get('NEW_RELIC_ENABLED', true) === 'true'
+            ? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Files_Logger))
+            : new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Files_Logger)),
+        )
+      container
+        .bind<DomainEventSubscriberFactoryInterface>(TYPES.Files_DomainEventSubscriberFactory)
+        .toConstantValue(
+          new SQSDomainEventSubscriberFactory(
+            container.get(TYPES.Files_SQS),
+            container.get(TYPES.Files_SQS_QUEUE_URL),
+            container.get(TYPES.Files_DomainEventMessageHandler),
+          ),
+        )
+    }
 
     return container
   }

+ 29 - 0
packages/files/src/Bootstrap/Service.ts

@@ -0,0 +1,29 @@
+import { ServiceContainerInterface, ServiceIdentifier, ServiceInterface } from '@standardnotes/domain-core'
+import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
+
+import { ContainerConfigLoader } from './Container'
+
+export class Service implements ServiceInterface {
+  constructor(
+    private serviceContainer: ServiceContainerInterface,
+    private directCallDomainEventPublisher: DirectCallDomainEventPublisher,
+  ) {
+    this.serviceContainer.register(this.getId(), this)
+  }
+
+  async handleRequest(_request: never, _response: never, _endpointOrMethodIdentifier: string): Promise<unknown> {
+    throw new Error('Requests are handled via inversify-express at ApiGateway level')
+  }
+
+  async getContainer(): Promise<unknown> {
+    const config = new ContainerConfigLoader()
+
+    return config.load({
+      directCallDomainEventPublisher: this.directCallDomainEventPublisher,
+    })
+  }
+
+  getId(): ServiceIdentifier {
+    return ServiceIdentifier.create(ServiceIdentifier.NAMES.Files).getValue()
+  }
+}

+ 41 - 39
packages/files/src/Bootstrap/Types.ts

@@ -1,55 +1,57 @@
 const TYPES = {
-  Logger: Symbol.for('Logger'),
-  HTTPClient: Symbol.for('HTTPClient'),
-  Redis: Symbol.for('Redis'),
-  S3: Symbol.for('S3'),
-  SNS: Symbol.for('SNS'),
-  SQS: Symbol.for('SQS'),
+  Files_Logger: Symbol.for('Files_Logger'),
+  Files_HTTPClient: Symbol.for('Files_HTTPClient'),
+  Files_Redis: Symbol.for('Files_Redis'),
+  Files_S3: Symbol.for('Files_S3'),
+  Files_SNS: Symbol.for('Files_SNS'),
+  Files_SQS: Symbol.for('Files_SQS'),
 
   // use cases
-  UploadFileChunk: Symbol.for('UploadFileChunk'),
-  StreamDownloadFile: Symbol.for('StreamDownloadFile'),
-  CreateUploadSession: Symbol.for('CreateUploadSession'),
-  FinishUploadSession: Symbol.for('FinishUploadSession'),
-  GetFileMetadata: Symbol.for('GetFileMetadata'),
-  RemoveFile: Symbol.for('RemoveFile'),
-  MarkFilesToBeRemoved: Symbol.for('MarkFilesToBeRemoved'),
+  Files_UploadFileChunk: Symbol.for('Files_UploadFileChunk'),
+  Files_StreamDownloadFile: Symbol.for('Files_StreamDownloadFile'),
+  Files_CreateUploadSession: Symbol.for('Files_CreateUploadSession'),
+  Files_FinishUploadSession: Symbol.for('Files_FinishUploadSession'),
+  Files_GetFileMetadata: Symbol.for('Files_GetFileMetadata'),
+  Files_RemoveFile: Symbol.for('Files_RemoveFile'),
+  Files_MarkFilesToBeRemoved: Symbol.for('Files_MarkFilesToBeRemoved'),
 
   // services
-  ValetTokenDecoder: Symbol.for('ValetTokenDecoder'),
-  Timer: Symbol.for('Timer'),
-  DomainEventFactory: Symbol.for('DomainEventFactory'),
-  DomainEventPublisher: Symbol.for('DomainEventPublisher'),
-  FileUploader: Symbol.for('FileUploader'),
-  FileDownloader: Symbol.for('FileDownloader'),
-  FileRemover: Symbol.for('FileRemover'),
+  Files_ValetTokenDecoder: Symbol.for('Files_ValetTokenDecoder'),
+  Files_Timer: Symbol.for('Files_Timer'),
+  Files_DomainEventFactory: Symbol.for('Files_DomainEventFactory'),
+  Files_DomainEventPublisher: Symbol.for('Files_DomainEventPublisher'),
+  Files_FileUploader: Symbol.for('Files_FileUploader'),
+  Files_FileDownloader: Symbol.for('Files_FileDownloader'),
+  Files_FileRemover: Symbol.for('Files_FileRemover'),
 
   // repositories
-  UploadRepository: Symbol.for('UploadRepository'),
+  Files_UploadRepository: Symbol.for('Files_UploadRepository'),
 
   // middleware
-  ValetTokenAuthMiddleware: Symbol.for('ValetTokenAuthMiddleware'),
+  Files_ValetTokenAuthMiddleware: Symbol.for('Files_ValetTokenAuthMiddleware'),
 
   // env vars
-  S3_ENDPOINT: Symbol.for('S3_ENDPOINT'),
-  S3_BUCKET_NAME: Symbol.for('S3_BUCKET_NAME'),
-  S3_AWS_REGION: Symbol.for('S3_AWS_REGION'),
-  SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
-  SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
-  SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
-  SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
-  VALET_TOKEN_SECRET: Symbol.for('VALET_TOKEN_SECRET'),
-  REDIS_URL: Symbol.for('REDIS_URL'),
-  MAX_CHUNK_BYTES: Symbol.for('MAX_CHUNK_BYTES'),
-  VERSION: Symbol.for('VERSION'),
-  NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
-  FILE_UPLOAD_PATH: Symbol.for('FILE_UPLOAD_PATH'),
+  Files_S3_ENDPOINT: Symbol.for('Files_S3_ENDPOINT'),
+  Files_S3_BUCKET_NAME: Symbol.for('Files_S3_BUCKET_NAME'),
+  Files_S3_AWS_REGION: Symbol.for('Files_S3_AWS_REGION'),
+  Files_SNS_TOPIC_ARN: Symbol.for('Files_SNS_TOPIC_ARN'),
+  Files_SNS_AWS_REGION: Symbol.for('Files_SNS_AWS_REGION'),
+  Files_SQS_QUEUE_URL: Symbol.for('Files_SQS_QUEUE_URL'),
+  Files_SQS_AWS_REGION: Symbol.for('Files_SQS_AWS_REGION'),
+  Files_VALET_TOKEN_SECRET: Symbol.for('Files_VALET_TOKEN_SECRET'),
+  Files_REDIS_URL: Symbol.for('Files_REDIS_URL'),
+  Files_MAX_CHUNK_BYTES: Symbol.for('Files_MAX_CHUNK_BYTES'),
+  Files_VERSION: Symbol.for('Files_VERSION'),
+  Files_NEW_RELIC_ENABLED: Symbol.for('Files_NEW_RELIC_ENABLED'),
+  Files_FILE_UPLOAD_PATH: Symbol.for('Files_FILE_UPLOAD_PATH'),
 
   // Handlers
-  DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
-  DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
-  AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
-  SharedSubscriptionInvitationCanceledEventHandler: Symbol.for('SharedSubscriptionInvitationCanceledEventHandler'),
+  Files_DomainEventMessageHandler: Symbol.for('Files_DomainEventMessageHandler'),
+  Files_DomainEventSubscriberFactory: Symbol.for('Files_DomainEventSubscriberFactory'),
+  Files_AccountDeletionRequestedEventHandler: Symbol.for('Files_AccountDeletionRequestedEventHandler'),
+  Files_SharedSubscriptionInvitationCanceledEventHandler: Symbol.for(
+    'Files_SharedSubscriptionInvitationCanceledEventHandler',
+  ),
 }
 
 export default TYPES

+ 2 - 0
packages/files/src/Bootstrap/index.ts

@@ -0,0 +1,2 @@
+export * from './Service'
+export * from './Types'

+ 0 - 12
packages/files/src/Controller/HealthCheckController.spec.ts

@@ -1,12 +0,0 @@
-import 'reflect-metadata'
-
-import { HealthCheckController } from './HealthCheckController'
-
-describe('HealthCheckController', () => {
-  const createController = () => new HealthCheckController()
-
-  it('should return OK', async () => {
-    const response = (await createController().get()) as string
-    expect(response).toEqual('OK')
-  })
-})

+ 1 - 1
packages/files/src/Domain/Event/DomainEventFactory.ts

@@ -7,7 +7,7 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 
 @injectable()
 export class DomainEventFactory implements DomainEventFactoryInterface {
-  constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
+  constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
 
   createFileRemovedEvent(payload: {
     userUuid: string

+ 3 - 3
packages/files/src/Domain/Handler/AccountDeletionRequestedEventHandler.ts

@@ -12,9 +12,9 @@ import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesT
 @injectable()
 export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
   constructor(
-    @inject(TYPES.MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
-    @inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
-    @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
+    @inject(TYPES.Files_MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
+    @inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
+    @inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
   ) {}
 
   async handle(event: AccountDeletionRequestedEvent): Promise<void> {

+ 3 - 3
packages/files/src/Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler.ts

@@ -12,9 +12,9 @@ import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesT
 @injectable()
 export class SharedSubscriptionInvitationCanceledEventHandler implements DomainEventHandlerInterface {
   constructor(
-    @inject(TYPES.MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
-    @inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
-    @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
+    @inject(TYPES.Files_MarkFilesToBeRemoved) private markFilesToBeRemoved: MarkFilesToBeRemoved,
+    @inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
+    @inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
   ) {}
 
   async handle(event: SharedSubscriptionInvitationCanceledEvent): Promise<void> {

+ 3 - 3
packages/files/src/Domain/UseCase/CreateUploadSession/CreateUploadSession.ts

@@ -11,9 +11,9 @@ import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterfac
 @injectable()
 export class CreateUploadSession implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileUploader) private fileUploader: FileUploaderInterface,
-    @inject(TYPES.UploadRepository) private uploadRepository: UploadRepositoryInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileUploader) private fileUploader: FileUploaderInterface,
+    @inject(TYPES.Files_UploadRepository) private uploadRepository: UploadRepositoryInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: CreateUploadSessionDTO): Promise<CreateUploadSessionResponse> {

+ 5 - 5
packages/files/src/Domain/UseCase/FinishUploadSession/FinishUploadSession.ts

@@ -13,11 +13,11 @@ import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInter
 @injectable()
 export class FinishUploadSession implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileUploader) private fileUploader: FileUploaderInterface,
-    @inject(TYPES.UploadRepository) private uploadRepository: UploadRepositoryInterface,
-    @inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
-    @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileUploader) private fileUploader: FileUploaderInterface,
+    @inject(TYPES.Files_UploadRepository) private uploadRepository: UploadRepositoryInterface,
+    @inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
+    @inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: FinishUploadSessionDTO): Promise<FinishUploadSessionResponse> {

+ 2 - 2
packages/files/src/Domain/UseCase/GetFileMetadata/GetFileMetadata.ts

@@ -9,8 +9,8 @@ import { GetFileMetadataResponse } from './GetFileMetadataResponse'
 @injectable()
 export class GetFileMetadata implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileDownloader) private fileDownloader: FileDownloaderInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: GetFileMetadataDTO): Promise<GetFileMetadataResponse> {

+ 2 - 2
packages/files/src/Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved.ts

@@ -10,8 +10,8 @@ import { MarkFilesToBeRemovedResponse } from './MarkFilesToBeRemovedResponse'
 @injectable()
 export class MarkFilesToBeRemoved implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileRemover) private fileRemover: FileRemoverInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileRemover) private fileRemover: FileRemoverInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: MarkFilesToBeRemovedDTO): Promise<MarkFilesToBeRemovedResponse> {

+ 4 - 4
packages/files/src/Domain/UseCase/RemoveFile/RemoveFile.ts

@@ -12,10 +12,10 @@ import { RemoveFileResponse } from './RemoveFileResponse'
 @injectable()
 export class RemoveFile implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileRemover) private fileRemover: FileRemoverInterface,
-    @inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
-    @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileRemover) private fileRemover: FileRemoverInterface,
+    @inject(TYPES.Files_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
+    @inject(TYPES.Files_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: RemoveFileDTO): Promise<RemoveFileResponse> {

+ 2 - 2
packages/files/src/Domain/UseCase/StreamDownloadFile/StreamDownloadFile.ts

@@ -9,8 +9,8 @@ import { StreamDownloadFileResponse } from './StreamDownloadFileResponse'
 @injectable()
 export class StreamDownloadFile implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileDownloader) private fileDownloader: FileDownloaderInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileDownloader) private fileDownloader: FileDownloaderInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: StreamDownloadFileDTO): Promise<StreamDownloadFileResponse> {

+ 3 - 3
packages/files/src/Domain/UseCase/UploadFileChunk/UploadFileChunk.ts

@@ -11,9 +11,9 @@ import { UploadRepositoryInterface } from '../../Upload/UploadRepositoryInterfac
 @injectable()
 export class UploadFileChunk implements UseCaseInterface {
   constructor(
-    @inject(TYPES.FileUploader) private fileUploader: FileUploaderInterface,
-    @inject(TYPES.UploadRepository) private uploadRepository: UploadRepositoryInterface,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FileUploader) private fileUploader: FileUploaderInterface,
+    @inject(TYPES.Files_UploadRepository) private uploadRepository: UploadRepositoryInterface,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {}
 
   async execute(dto: UploadFileChunkDTO): Promise<UploadFileChunkResponse> {

+ 1 - 1
packages/files/src/Infra/FS/FSFileDownloader.ts

@@ -7,7 +7,7 @@ import TYPES from '../../Bootstrap/Types'
 
 @injectable()
 export class FSFileDownloader implements FileDownloaderInterface {
-  constructor(@inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string) {}
+  constructor(@inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string) {}
 
   async getFileSize(filePath: string): Promise<number> {
     return (await promises.stat(`${this.fileUploadPath}/${filePath}`)).size

+ 1 - 1
packages/files/src/Infra/FS/FSFileRemover.ts

@@ -7,7 +7,7 @@ import TYPES from '../../Bootstrap/Types'
 
 @injectable()
 export class FSFileRemover implements FileRemoverInterface {
-  constructor(@inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string) {}
+  constructor(@inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string) {}
 
   async markFilesToBeRemoved(userUuid: string): Promise<Array<RemovedFileDescription>> {
     await promises.rmdir(`${this.fileUploadPath}/${userUuid}`)

+ 2 - 2
packages/files/src/Infra/FS/FSFileUploader.ts

@@ -13,8 +13,8 @@ export class FSFileUploader implements FileUploaderInterface {
   private inMemoryChunks: Map<string, Map<number, Uint8Array>>
 
   constructor(
-    @inject(TYPES.FILE_UPLOAD_PATH) private fileUploadPath: string,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_FILE_UPLOAD_PATH) private fileUploadPath: string,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {
     this.inMemoryChunks = new Map<string, Map<number, Uint8Array>>()
   }

+ 9 - 9
packages/files/src/Controller/FilesController.spec.ts → packages/files/src/Infra/InversifyExpress/InversifyExpressFilesController.spec.ts

@@ -1,20 +1,20 @@
 import 'reflect-metadata'
 
-import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
-import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
-import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
-import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
+import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
+import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
+import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
+import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
 
 import { Request, Response } from 'express'
 import { Writable, Readable } from 'stream'
-import { FilesController } from './FilesController'
-import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
+import { InversifyExpressFilesController } from './InversifyExpressFilesController'
+import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
 import { results } from 'inversify-express-utils'
-import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
+import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
 import { ValetTokenOperation } from '@standardnotes/security'
 import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
 
-describe('FilesController', () => {
+describe('InversifyExpressFilesController', () => {
   let uploadFileChunk: UploadFileChunk
   let createUploadSession: CreateUploadSession
   let finishUploadSession: FinishUploadSession
@@ -27,7 +27,7 @@ describe('FilesController', () => {
   const maxChunkBytes = 100_000
 
   const createController = () =>
-    new FilesController(
+    new InversifyExpressFilesController(
       uploadFileChunk,
       createUploadSession,
       finishUploadSession,

+ 16 - 16
packages/files/src/Controller/FilesController.ts → packages/files/src/Infra/InversifyExpress/InversifyExpressFilesController.ts

@@ -2,25 +2,25 @@ import { BaseHttpController, controller, httpDelete, httpGet, httpPost, results
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { Writable } from 'stream'
-import TYPES from '../Bootstrap/Types'
-import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
-import { StreamDownloadFile } from '../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
-import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/CreateUploadSession'
-import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
-import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
-import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
+import TYPES from '../../Bootstrap/Types'
+import { UploadFileChunk } from '../../Domain/UseCase/UploadFileChunk/UploadFileChunk'
+import { StreamDownloadFile } from '../../Domain/UseCase/StreamDownloadFile/StreamDownloadFile'
+import { CreateUploadSession } from '../../Domain/UseCase/CreateUploadSession/CreateUploadSession'
+import { FinishUploadSession } from '../../Domain/UseCase/FinishUploadSession/FinishUploadSession'
+import { GetFileMetadata } from '../../Domain/UseCase/GetFileMetadata/GetFileMetadata'
+import { RemoveFile } from '../../Domain/UseCase/RemoveFile/RemoveFile'
 import { ValetTokenOperation } from '@standardnotes/security'
 
-@controller('/v1/files', TYPES.ValetTokenAuthMiddleware)
-export class FilesController extends BaseHttpController {
+@controller('/v1/files', TYPES.Files_ValetTokenAuthMiddleware)
+export class InversifyExpressFilesController extends BaseHttpController {
   constructor(
-    @inject(TYPES.UploadFileChunk) private uploadFileChunk: UploadFileChunk,
-    @inject(TYPES.CreateUploadSession) private createUploadSession: CreateUploadSession,
-    @inject(TYPES.FinishUploadSession) private finishUploadSession: FinishUploadSession,
-    @inject(TYPES.StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
-    @inject(TYPES.GetFileMetadata) private getFileMetadata: GetFileMetadata,
-    @inject(TYPES.RemoveFile) private removeFile: RemoveFile,
-    @inject(TYPES.MAX_CHUNK_BYTES) private maxChunkBytes: number,
+    @inject(TYPES.Files_UploadFileChunk) private uploadFileChunk: UploadFileChunk,
+    @inject(TYPES.Files_CreateUploadSession) private createUploadSession: CreateUploadSession,
+    @inject(TYPES.Files_FinishUploadSession) private finishUploadSession: FinishUploadSession,
+    @inject(TYPES.Files_StreamDownloadFile) private streamDownloadFile: StreamDownloadFile,
+    @inject(TYPES.Files_GetFileMetadata) private getFileMetadata: GetFileMetadata,
+    @inject(TYPES.Files_RemoveFile) private removeFile: RemoveFile,
+    @inject(TYPES.Files_MAX_CHUNK_BYTES) private maxChunkBytes: number,
   ) {
     super()
   }

+ 12 - 0
packages/files/src/Infra/InversifyExpress/InversifyExpressHealthCheckController.spec.ts

@@ -0,0 +1,12 @@
+import 'reflect-metadata'
+
+import { InversifyExpressHealthCheckController } from './InversifyExpressHealthCheckController'
+
+describe('InversifyExpressHealthCheckController', () => {
+  const createController = () => new InversifyExpressHealthCheckController()
+
+  it('should return OK', async () => {
+    const response = (await createController().get()) as string
+    expect(response).toEqual('OK')
+  })
+})

+ 1 - 1
packages/files/src/Controller/HealthCheckController.ts → packages/files/src/Infra/InversifyExpress/InversifyExpressHealthCheckController.ts

@@ -1,7 +1,7 @@
 import { controller, httpGet } from 'inversify-express-utils'
 
 @controller('/healthcheck')
-export class HealthCheckController {
+export class InversifyExpressHealthCheckController {
   @httpGet('/')
   public async get(): Promise<string> {
     return 'OK'

+ 0 - 0
packages/files/src/Controller/ValetTokenAuthMiddleware.spec.ts → packages/files/src/Infra/InversifyExpress/Middleware/ValetTokenAuthMiddleware.spec.ts


+ 3 - 3
packages/files/src/Controller/ValetTokenAuthMiddleware.ts → packages/files/src/Infra/InversifyExpress/Middleware/ValetTokenAuthMiddleware.ts

@@ -4,13 +4,13 @@ import { NextFunction, Request, Response } from 'express'
 import { inject, injectable } from 'inversify'
 import { BaseMiddleware } from 'inversify-express-utils'
 import { Logger } from 'winston'
-import TYPES from '../Bootstrap/Types'
+import TYPES from '../../../Bootstrap/Types'
 
 @injectable()
 export class ValetTokenAuthMiddleware extends BaseMiddleware {
   constructor(
-    @inject(TYPES.ValetTokenDecoder) private tokenDecoder: TokenDecoderInterface<ValetTokenData>,
-    @inject(TYPES.Logger) private logger: Logger,
+    @inject(TYPES.Files_ValetTokenDecoder) private tokenDecoder: TokenDecoderInterface<ValetTokenData>,
+    @inject(TYPES.Files_Logger) private logger: Logger,
   ) {
     super()
   }

+ 1 - 0
packages/files/src/Infra/InversifyExpress/index.ts

@@ -0,0 +1 @@
+export * from './InversifyExpressFilesController'

+ 1 - 1
packages/files/src/Infra/Redis/RedisUploadRepository.ts

@@ -10,7 +10,7 @@ export class RedisUploadRepository implements UploadRepositoryInterface {
   private readonly UPLOAD_CHUNKS_PREFIX = 'upload-chunks'
   private readonly UPLOAD_SESSION_DEFAULT_TTL = 7200
 
-  constructor(@inject(TYPES.Redis) private redisClient: IORedis.Redis) {}
+  constructor(@inject(TYPES.Files_Redis) private redisClient: IORedis.Redis) {}
 
   async storeUploadSession(filePath: string, uploadId: string): Promise<void> {
     await this.redisClient.setex(`${this.UPLOAD_SESSION_PREFIX}:${filePath}`, this.UPLOAD_SESSION_DEFAULT_TTL, uploadId)

+ 2 - 2
packages/files/src/Infra/S3/S3FileDownloader.ts

@@ -8,8 +8,8 @@ import { FileDownloaderInterface } from '../../Domain/Services/FileDownloaderInt
 @injectable()
 export class S3FileDownloader implements FileDownloaderInterface {
   constructor(
-    @inject(TYPES.S3) private s3Client: S3Client,
-    @inject(TYPES.S3_BUCKET_NAME) private s3BuckeName: string,
+    @inject(TYPES.Files_S3) private s3Client: S3Client,
+    @inject(TYPES.Files_S3_BUCKET_NAME) private s3BuckeName: string,
   ) {}
 
   async createDownloadStream(filePath: string, startRange: number, endRange: number): Promise<Readable> {

+ 2 - 2
packages/files/src/Infra/S3/S3FileRemover.ts

@@ -14,8 +14,8 @@ import { RemovedFileDescription } from '../../Domain/File/RemovedFileDescription
 @injectable()
 export class S3FileRemover implements FileRemoverInterface {
   constructor(
-    @inject(TYPES.S3) private s3Client: S3Client,
-    @inject(TYPES.S3_BUCKET_NAME) private s3BuckeName: string,
+    @inject(TYPES.Files_S3) private s3Client: S3Client,
+    @inject(TYPES.Files_S3_BUCKET_NAME) private s3BuckeName: string,
   ) {}
 
   async markFilesToBeRemoved(userUuid: string): Promise<Array<RemovedFileDescription>> {

+ 2 - 2
packages/files/src/Infra/S3/S3FileUploader.ts

@@ -15,8 +15,8 @@ import { ChunkId } from '../../Domain/Upload/ChunkId'
 @injectable()
 export class S3FileUploader implements FileUploaderInterface {
   constructor(
-    @inject(TYPES.S3) private s3Client: S3Client,
-    @inject(TYPES.S3_BUCKET_NAME) private s3BuckeName: string,
+    @inject(TYPES.Files_S3) private s3Client: S3Client,
+    @inject(TYPES.Files_S3_BUCKET_NAME) private s3BuckeName: string,
   ) {}
 
   async createUploadSession(filePath: string): Promise<UploadId> {

+ 2 - 0
packages/files/src/index.ts

@@ -0,0 +1,2 @@
+export * from './Bootstrap'
+export * from './Infra/InversifyExpress'

+ 1 - 0
packages/home-server/.env.sample

@@ -15,5 +15,6 @@ JWT_SECRET=
 AUTH_JWT_SECRET=
 ENCRYPTION_SERVER_KEY=
 PSEUDO_KEY_PARAMS_KEY=
+VALET_TOKEN_SECRET=
 
 FILES_SERVER_URL=

+ 3 - 0
packages/home-server/bin/server.ts

@@ -2,6 +2,7 @@ import 'reflect-metadata'
 
 import { ControllerContainer, ServiceContainer } from '@standardnotes/domain-core'
 import { Service as ApiGatewayService, TYPES as ApiGatewayTYPES } from '@standardnotes/api-gateway'
+import { Service as FilesService } from '@standardnotes/files-server'
 import { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
 import { Service as AuthService } from '@standardnotes/auth-server'
 import { Service as SyncingService } from '@standardnotes/syncing-server'
@@ -26,12 +27,14 @@ const startServer = async (): Promise<void> => {
   const authService = new AuthService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
   const syncingService = new SyncingService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
   const revisionsService = new RevisionsService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
+  const filesService = new FilesService(serviceContainer, directCallDomainEventPublisher)
 
   const container = Container.merge(
     (await apiGatewayService.getContainer()) as Container,
     (await authService.getContainer()) as Container,
     (await syncingService.getContainer()) as Container,
     (await revisionsService.getContainer()) as Container,
+    (await filesService.getContainer()) as Container,
   )
 
   const env: Env = new Env()

+ 1 - 0
packages/home-server/package.json

@@ -22,6 +22,7 @@
     "@standardnotes/auth-server": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events-infra": "workspace:^",
+    "@standardnotes/files-server": "workspace:^",
     "@standardnotes/revisions-server": "workspace:^",
     "@standardnotes/syncing-server": "workspace:^",
     "cors": "2.8.5",

+ 3 - 3
packages/revisions/src/Bootstrap/Container.ts

@@ -45,7 +45,7 @@ import { CopyRevisions } from '../Domain/UseCase/CopyRevisions/CopyRevisions'
 import { FSDumpRepository } from '../Infra/FS/FSDumpRepository'
 import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
 import { RevisionItemStringMapper } from '../Mapping/RevisionItemStringMapper'
-import { InversifyExpressRevisionsController } from '../Infra/InversifyExpress/InversifyExpressRevisionsController'
+import { HomeServerRevisionsController } from '../Infra/InversifyExpress/HomeServer/HomeServerRevisionsController'
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -337,9 +337,9 @@ export class ContainerConfigLoader {
     // Inversify Controllers
     if (isConfiguredForHomeServer) {
       container
-        .bind<InversifyExpressRevisionsController>(TYPES.Revisions_InversifyExpressRevisionsController)
+        .bind<HomeServerRevisionsController>(TYPES.Revisions_HomeServerRevisionsController)
         .toConstantValue(
-          new InversifyExpressRevisionsController(
+          new HomeServerRevisionsController(
             container.get(TYPES.Revisions_RevisionsController),
             container.get(TYPES.Revisions_ControllerContainer),
           ),

+ 1 - 1
packages/revisions/src/Bootstrap/Types.ts

@@ -43,7 +43,7 @@ const TYPES = {
   Revisions_DomainEventMessageHandler: Symbol.for('Revisions_DomainEventMessageHandler'),
   Revisions_Timer: Symbol.for('Revisions_Timer'),
   // Inversify Express Controllers
-  Revisions_InversifyExpressRevisionsController: Symbol.for('Revisions_InversifyExpressRevisionsController'),
+  Revisions_HomeServerRevisionsController: Symbol.for('Revisions_HomeServerRevisionsController'),
 }
 
 export default TYPES

+ 47 - 0
packages/revisions/src/Infra/InversifyExpress/HomeServer/HomeServerRevisionsController.ts

@@ -0,0 +1,47 @@
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { Request, Response } from 'express'
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+
+import { RevisionsController } from '../../../Controller/RevisionsController'
+
+export class HomeServerRevisionsController extends BaseHttpController {
+  constructor(
+    protected revisionsController: RevisionsController,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('revisions.revisions.getRevisions', this.getRevisions.bind(this))
+      this.controllerContainer.register('revisions.revisions.getRevision', this.getRevision.bind(this))
+      this.controllerContainer.register('revisions.revisions.deleteRevision', this.deleteRevision.bind(this))
+    }
+  }
+
+  async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.revisionsController.getRevisions({
+      itemUuid: req.params.itemUuid,
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async getRevision(req: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.revisionsController.getRevision({
+      revisionUuid: req.params.uuid,
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+
+  async deleteRevision(req: Request, response: Response): Promise<results.JsonResult> {
+    const result = await this.revisionsController.deleteRevision({
+      revisionUuid: req.params.uuid,
+      userUuid: response.locals.user.uuid,
+    })
+
+    return this.json(result.data, result.status)
+  }
+}

+ 11 - 33
packages/revisions/src/Infra/InversifyExpress/InversifyExpressRevisionsController.ts

@@ -1,51 +1,29 @@
 import { Request, Response } from 'express'
-import { BaseHttpController, controller, httpDelete, httpGet, results } from 'inversify-express-utils'
+import { controller, httpDelete, httpGet, results } from 'inversify-express-utils'
 import { inject } from 'inversify'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 import TYPES from '../../Bootstrap/Types'
 import { RevisionsController } from '../../Controller/RevisionsController'
+import { HomeServerRevisionsController } from './HomeServer/HomeServerRevisionsController'
 
 @controller('/items/:itemUuid/revisions', TYPES.Revisions_ApiGatewayAuthMiddleware)
-export class InversifyExpressRevisionsController extends BaseHttpController {
-  constructor(
-    @inject(TYPES.Revisions_RevisionsController) private revisionsController: RevisionsController,
-    @inject(TYPES.Revisions_ControllerContainer) private controllerContainer: ControllerContainerInterface,
-  ) {
-    super()
-
-    this.controllerContainer.register('revisions.revisions.getRevisions', this.getRevisions.bind(this))
-    this.controllerContainer.register('revisions.revisions.getRevision', this.getRevision.bind(this))
-    this.controllerContainer.register('revisions.revisions.deleteRevision', this.deleteRevision.bind(this))
+export class InversifyExpressRevisionsController extends HomeServerRevisionsController {
+  constructor(@inject(TYPES.Revisions_RevisionsController) override revisionsController: RevisionsController) {
+    super(revisionsController)
   }
 
   @httpGet('/')
-  public async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.revisionsController.getRevisions({
-      itemUuid: req.params.itemUuid,
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(result.data, result.status)
+  override async getRevisions(req: Request, response: Response): Promise<results.JsonResult> {
+    return super.getRevisions(req, response)
   }
 
   @httpGet('/:uuid')
-  public async getRevision(req: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.revisionsController.getRevision({
-      revisionUuid: req.params.uuid,
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(result.data, result.status)
+  override async getRevision(req: Request, response: Response): Promise<results.JsonResult> {
+    return super.getRevision(req, response)
   }
 
   @httpDelete('/:uuid')
-  public async deleteRevision(req: Request, response: Response): Promise<results.JsonResult> {
-    const result = await this.revisionsController.deleteRevision({
-      revisionUuid: req.params.uuid,
-      userUuid: response.locals.user.uuid,
-    })
-
-    return this.json(result.data, result.status)
+  override async deleteRevision(req: Request, response: Response): Promise<results.JsonResult> {
+    return super.deleteRevision(req, response)
   }
 }

+ 3 - 3
packages/syncing-server/src/Bootstrap/Container.ts

@@ -71,7 +71,7 @@ import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
 import { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
 import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
 import { ControllerContainer, ControllerContainerInterface } from '@standardnotes/domain-core'
-import { InversifyExpressItemsController } from '../Infra/InversifyExpressUtils/InversifyExpressItemsController'
+import { HomeServerItemsController } from '../Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController'
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
 
@@ -499,9 +499,9 @@ export class ContainerConfigLoader {
 
     if (isConfiguredForHomeServer) {
       container
-        .bind<InversifyExpressItemsController>(TYPES.Sync_InversifyExpressItemsController)
+        .bind<HomeServerItemsController>(TYPES.Sync_HomeServerItemsController)
         .toConstantValue(
-          new InversifyExpressItemsController(
+          new HomeServerItemsController(
             container.get(TYPES.Sync_SyncItems),
             container.get(TYPES.Sync_CheckIntegrity),
             container.get(TYPES.Sync_GetItem),

+ 1 - 1
packages/syncing-server/src/Bootstrap/Types.ts

@@ -67,7 +67,7 @@ const TYPES = {
   Sync_ItemFactory: Symbol.for('Sync_ItemFactory'),
   Sync_ItemTransferCalculator: Symbol.for('Sync_ItemTransferCalculator'),
   Sync_ControllerContainer: Symbol.for('Sync_ControllerContainer'),
-  Sync_InversifyExpressItemsController: Symbol.for('Sync_InversifyExpressItemsController'),
+  Sync_HomeServerItemsController: Symbol.for('Sync_HomeServerItemsController'),
 }
 
 export default TYPES

+ 85 - 0
packages/syncing-server/src/Infra/InversifyExpressUtils/HomeServer/HomeServerItemsController.ts

@@ -0,0 +1,85 @@
+import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { BaseHttpController, results } from 'inversify-express-utils'
+import { Request, Response } from 'express'
+
+import { Item } from '../../../Domain/Item/Item'
+import { SyncResponseFactoryResolverInterface } from '../../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
+import { CheckIntegrity } from '../../../Domain/UseCase/CheckIntegrity/CheckIntegrity'
+import { GetItem } from '../../../Domain/UseCase/GetItem/GetItem'
+import { SyncItems } from '../../../Domain/UseCase/SyncItems'
+import { ItemProjection } from '../../../Projection/ItemProjection'
+import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
+import { ApiVersion } from '../../../Domain/Api/ApiVersion'
+
+export class HomeServerItemsController extends BaseHttpController {
+  constructor(
+    protected syncItems: SyncItems,
+    protected checkIntegrity: CheckIntegrity,
+    protected getItem: GetItem,
+    protected itemProjector: ProjectorInterface<Item, ItemProjection>,
+    protected syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
+    private controllerContainer?: ControllerContainerInterface,
+  ) {
+    super()
+
+    if (this.controllerContainer !== undefined) {
+      this.controllerContainer.register('sync.items.sync', this.sync.bind(this))
+      this.controllerContainer.register('sync.items.check_integrity', this.checkItemsIntegrity.bind(this))
+      this.controllerContainer.register('sync.items.get_item', this.getSingleItem.bind(this))
+    }
+  }
+
+  async sync(request: Request, response: Response): Promise<results.JsonResult> {
+    let itemHashes = []
+    if ('items' in request.body) {
+      itemHashes = request.body.items
+    }
+
+    const syncResult = await this.syncItems.execute({
+      userUuid: response.locals.user.uuid,
+      itemHashes,
+      computeIntegrityHash: request.body.compute_integrity === true,
+      syncToken: request.body.sync_token,
+      cursorToken: request.body.cursor_token,
+      limit: request.body.limit,
+      contentType: request.body.content_type,
+      apiVersion: request.body.api ?? ApiVersion.v20161215,
+      readOnlyAccess: response.locals.readOnlyAccess,
+      sessionUuid: response.locals.session ? response.locals.session.uuid : null,
+    })
+
+    const syncResponse = await this.syncResponseFactoryResolver
+      .resolveSyncResponseFactoryVersion(request.body.api)
+      .createResponse(syncResult)
+
+    return this.json(syncResponse)
+  }
+
+  async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
+    let integrityPayloads = []
+    if ('integrityPayloads' in request.body) {
+      integrityPayloads = request.body.integrityPayloads
+    }
+
+    const result = await this.checkIntegrity.execute({
+      userUuid: response.locals.user.uuid,
+      integrityPayloads,
+      freeUser: response.locals.freeUser,
+    })
+
+    return this.json(result)
+  }
+
+  async getSingleItem(request: Request, response: Response): Promise<results.NotFoundResult | results.JsonResult> {
+    const result = await this.getItem.execute({
+      userUuid: response.locals.user.uuid,
+      itemUuid: request.params.uuid,
+    })
+
+    if (!result.success) {
+      return this.notFound()
+    }
+
+    return this.json({ item: await this.itemProjector.projectFull(result.item) })
+  }
+}

+ 1 - 13
packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.spec.ts

@@ -15,7 +15,6 @@ import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResp
 import { CheckIntegrity } from '../../Domain/UseCase/CheckIntegrity/CheckIntegrity'
 import { GetItem } from '../../Domain/UseCase/GetItem/GetItem'
 import { SyncItems } from '../../Domain/UseCase/SyncItems'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 describe('InversifyExpressItemsController', () => {
   let syncItems: SyncItems
@@ -27,22 +26,11 @@ describe('InversifyExpressItemsController', () => {
   let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
   let syncResponseFactory: SyncResponseFactoryInterface
   let syncResponse: SyncResponse20200115
-  let controllerContainer: ControllerContainerInterface
 
   const createController = () =>
-    new InversifyExpressItemsController(
-      syncItems,
-      checkIntegrity,
-      getItem,
-      itemProjector,
-      syncResponceFactoryResolver,
-      controllerContainer,
-    )
+    new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, itemProjector, syncResponceFactoryResolver)
 
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
     itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
 

+ 15 - 63
packages/syncing-server/src/Infra/InversifyExpressUtils/InversifyExpressItemsController.ts

@@ -1,6 +1,6 @@
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
-import { BaseHttpController, controller, httpGet, httpPost, results } from 'inversify-express-utils'
+import { controller, httpGet, httpPost, results } from 'inversify-express-utils'
 
 import TYPES from '../../Bootstrap/Types'
 import { Item } from '../../Domain/Item/Item'
@@ -10,84 +10,36 @@ import { GetItem } from '../../Domain/UseCase/GetItem/GetItem'
 import { SyncItems } from '../../Domain/UseCase/SyncItems'
 import { ItemProjection } from '../../Projection/ItemProjection'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ApiVersion } from '../../Domain/Api/ApiVersion'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerItemsController } from './HomeServer/HomeServerItemsController'
 
 @controller('/items', TYPES.Sync_AuthMiddleware)
-export class InversifyExpressItemsController extends BaseHttpController {
+export class InversifyExpressItemsController extends HomeServerItemsController {
   constructor(
-    @inject(TYPES.Sync_SyncItems) private syncItems: SyncItems,
-    @inject(TYPES.Sync_CheckIntegrity) private checkIntegrity: CheckIntegrity,
-    @inject(TYPES.Sync_GetItem) private getItem: GetItem,
-    @inject(TYPES.Sync_ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
+    @inject(TYPES.Sync_SyncItems) override syncItems: SyncItems,
+    @inject(TYPES.Sync_CheckIntegrity) override checkIntegrity: CheckIntegrity,
+    @inject(TYPES.Sync_GetItem) override getItem: GetItem,
+    @inject(TYPES.Sync_ItemProjector) override itemProjector: ProjectorInterface<Item, ItemProjection>,
     @inject(TYPES.Sync_SyncResponseFactoryResolver)
-    private syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
-    @inject(TYPES.Sync_ControllerContainer) private controllerContainer: ControllerContainerInterface,
+    override syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
   ) {
-    super()
-
-    this.controllerContainer.register('sync.items.sync', this.sync.bind(this))
-    this.controllerContainer.register('sync.items.check_integrity', this.checkItemsIntegrity.bind(this))
-    this.controllerContainer.register('sync.items.get_item', this.getSingleItem.bind(this))
+    super(syncItems, checkIntegrity, getItem, itemProjector, syncResponseFactoryResolver)
   }
 
   @httpPost('/sync')
-  public async sync(request: Request, response: Response): Promise<results.JsonResult> {
-    let itemHashes = []
-    if ('items' in request.body) {
-      itemHashes = request.body.items
-    }
-
-    const syncResult = await this.syncItems.execute({
-      userUuid: response.locals.user.uuid,
-      itemHashes,
-      computeIntegrityHash: request.body.compute_integrity === true,
-      syncToken: request.body.sync_token,
-      cursorToken: request.body.cursor_token,
-      limit: request.body.limit,
-      contentType: request.body.content_type,
-      apiVersion: request.body.api ?? ApiVersion.v20161215,
-      readOnlyAccess: response.locals.readOnlyAccess,
-      sessionUuid: response.locals.session ? response.locals.session.uuid : null,
-    })
-
-    const syncResponse = await this.syncResponseFactoryResolver
-      .resolveSyncResponseFactoryVersion(request.body.api)
-      .createResponse(syncResult)
-
-    return this.json(syncResponse)
+  override async sync(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.sync(request, response)
   }
 
   @httpPost('/check-integrity')
-  public async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
-    let integrityPayloads = []
-    if ('integrityPayloads' in request.body) {
-      integrityPayloads = request.body.integrityPayloads
-    }
-
-    const result = await this.checkIntegrity.execute({
-      userUuid: response.locals.user.uuid,
-      integrityPayloads,
-      freeUser: response.locals.freeUser,
-    })
-
-    return this.json(result)
+  override async checkItemsIntegrity(request: Request, response: Response): Promise<results.JsonResult> {
+    return super.checkItemsIntegrity(request, response)
   }
 
   @httpGet('/:uuid')
-  public async getSingleItem(
+  override async getSingleItem(
     request: Request,
     response: Response,
   ): Promise<results.NotFoundResult | results.JsonResult> {
-    const result = await this.getItem.execute({
-      userUuid: response.locals.user.uuid,
-      itemUuid: request.params.uuid,
-    })
-
-    if (!result.success) {
-      return this.notFound()
-    }
-
-    return this.json({ item: await this.itemProjector.projectFull(result.item) })
+    return super.getSingleItem(request, response)
   }
 }

+ 2 - 1
yarn.lock

@@ -4188,7 +4188,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/files-server@workspace:packages/files":
+"@standardnotes/files-server@workspace:^, @standardnotes/files-server@workspace:packages/files":
   version: 0.0.0-use.local
   resolution: "@standardnotes/files-server@workspace:packages/files"
   dependencies:
@@ -4249,6 +4249,7 @@ __metadata:
     "@standardnotes/auth-server": "workspace:^"
     "@standardnotes/domain-core": "workspace:^"
     "@standardnotes/domain-events-infra": "workspace:^"
+    "@standardnotes/files-server": "workspace:^"
     "@standardnotes/revisions-server": "workspace:^"
     "@standardnotes/syncing-server": "workspace:^"
     "@types/cors": "npm:^2.8.9"