Przeglądaj źródła

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 lat temu
rodzic
commit
c7d575a0ff
95 zmienionych plików z 2460 dodań i 1895 usunięć
  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/auth-server", "workspace:packages/auth"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
           ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
+          ["@standardnotes/files-server", "workspace:packages/files"],\
           ["@standardnotes/revisions-server", "workspace:packages/revisions"],\
           ["@standardnotes/revisions-server", "workspace:packages/revisions"],\
           ["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
           ["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
           ["@types/cors", "npm:2.8.13"],\
           ["@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 './Service'
 export * from './Types'
 export * from './Types'

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

@@ -1,6 +1,5 @@
 export * from './AuthMiddleware'
 export * from './AuthMiddleware'
 export * from './HealthCheckController'
 export * from './HealthCheckController'
-export * from './LegacyController'
 export * from './SubscriptionTokenAuthMiddleware'
 export * from './SubscriptionTokenAuthMiddleware'
 export * from './TokenAuthenticationMethod'
 export * from './TokenAuthenticationMethod'
 export * from './WebSocketAuthMiddleware'
 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 { TypeORMOfflineSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMOfflineSubscriptionTokenRepository'
 import { TypeORMPKCERepository } from '../Infra/TypeORM/TypeORMPKCERepository'
 import { TypeORMPKCERepository } from '../Infra/TypeORM/TypeORMPKCERepository'
 import { TypeORMSubscriptionTokenRepository } from '../Infra/TypeORM/TypeORMSubscriptionTokenRepository'
 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 { SessionMiddleware } from '../Infra/InversifyExpressUtils/Middleware/SessionMiddleware'
 import { ApiGatewayOfflineAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware'
 import { ApiGatewayOfflineAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/ApiGatewayOfflineAuthMiddleware'
 import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware'
 import { OfflineUserAuthMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OfflineUserAuthMiddleware'
 import { LockMiddleware } from '../Infra/InversifyExpressUtils/Middleware/LockMiddleware'
 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 { RequiredCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/RequiredCrossServiceTokenMiddleware'
 import { OptionalCrossServiceTokenMiddleware } from '../Infra/InversifyExpressUtils/Middleware/OptionalCrossServiceTokenMiddleware'
 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
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
 const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -1012,9 +1011,9 @@ export class ContainerConfigLoader {
     }
     }
 
 
     container
     container
-      .bind<InversifyExpressAuthController>(TYPES.Auth_InversifyExpressAuthController)
+      .bind<HomeServerAuthController>(TYPES.Auth_HomeServerAuthController)
       .toConstantValue(
       .toConstantValue(
-        new InversifyExpressAuthController(
+        new HomeServerAuthController(
           container.get(TYPES.Auth_VerifyMFA),
           container.get(TYPES.Auth_VerifyMFA),
           container.get(TYPES.Auth_SignIn),
           container.get(TYPES.Auth_SignIn),
           container.get(TYPES.Auth_GetUserKeyParams),
           container.get(TYPES.Auth_GetUserKeyParams),
@@ -1029,42 +1028,42 @@ export class ContainerConfigLoader {
     // Inversify Controllers
     // Inversify Controllers
     if (isConfiguredForHomeServer) {
     if (isConfiguredForHomeServer) {
       container
       container
-        .bind<InversifyExpressAuthenticatorsController>(TYPES.Auth_InversifyExpressAuthenticatorsController)
+        .bind<HomeServerAuthenticatorsController>(TYPES.Auth_HomeServerAuthenticatorsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressAuthenticatorsController(
+          new HomeServerAuthenticatorsController(
             container.get(TYPES.Auth_AuthenticatorsController),
             container.get(TYPES.Auth_AuthenticatorsController),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressSubscriptionInvitesController>(TYPES.Auth_InversifyExpressSubscriptionInvitesController)
+        .bind<HomeServerSubscriptionInvitesController>(TYPES.Auth_HomeServerSubscriptionInvitesController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressSubscriptionInvitesController(
+          new HomeServerSubscriptionInvitesController(
             container.get(TYPES.Auth_SubscriptionInvitesController),
             container.get(TYPES.Auth_SubscriptionInvitesController),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressUserRequestsController>(TYPES.Auth_InversifyExpressUserRequestsController)
+        .bind<HomeServerUserRequestsController>(TYPES.Auth_HomeServerUserRequestsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressUserRequestsController(
+          new HomeServerUserRequestsController(
             container.get(TYPES.Auth_UserRequestsController),
             container.get(TYPES.Auth_UserRequestsController),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressWebSocketsController>(TYPES.Auth_InversifyExpressWebSocketsController)
+        .bind<HomeServerWebSocketsController>(TYPES.Auth_HomeServerWebSocketsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressWebSocketsController(
+          new HomeServerWebSocketsController(
             container.get(TYPES.Auth_CreateCrossServiceToken),
             container.get(TYPES.Auth_CreateCrossServiceToken),
             container.get(TYPES.Auth_WebSocketConnectionTokenDecoder),
             container.get(TYPES.Auth_WebSocketConnectionTokenDecoder),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressSessionsController>(TYPES.Auth_SessionsController)
+        .bind<HomeServerSessionsController>(TYPES.Auth_HomeServerSessionsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressSessionsController(
+          new HomeServerSessionsController(
             container.get(TYPES.Auth_GetActiveSessionsForUser),
             container.get(TYPES.Auth_GetActiveSessionsForUser),
             container.get(TYPES.Auth_AuthenticateRequest),
             container.get(TYPES.Auth_AuthenticateRequest),
             container.get(TYPES.Auth_SessionProjector),
             container.get(TYPES.Auth_SessionProjector),
@@ -1073,17 +1072,17 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressValetTokenController>(TYPES.Auth_InversifyExpressValetTokenController)
+        .bind<HomeServerValetTokenController>(TYPES.Auth_HomeServerValetTokenController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressValetTokenController(
+          new HomeServerValetTokenController(
             container.get(TYPES.Auth_CreateValetToken),
             container.get(TYPES.Auth_CreateValetToken),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressUsersController>(TYPES.Auth_InversifyExpressUsersController)
+        .bind<HomeServerUsersController>(TYPES.Auth_HomeServerUsersController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressUsersController(
+          new HomeServerUsersController(
             container.get(TYPES.Auth_UpdateUser),
             container.get(TYPES.Auth_UpdateUser),
             container.get(TYPES.Auth_GetUserKeyParams),
             container.get(TYPES.Auth_GetUserKeyParams),
             container.get(TYPES.Auth_DeleteAccount),
             container.get(TYPES.Auth_DeleteAccount),
@@ -1095,9 +1094,9 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressAdminController>(TYPES.Auth_InversifyExpressAdminController)
+        .bind<HomeServerAdminController>(TYPES.Auth_HomeServerAdminController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressAdminController(
+          new HomeServerAdminController(
             container.get(TYPES.Auth_DeleteSetting),
             container.get(TYPES.Auth_DeleteSetting),
             container.get(TYPES.Auth_UserRepository),
             container.get(TYPES.Auth_UserRepository),
             container.get(TYPES.Auth_CreateSubscriptionToken),
             container.get(TYPES.Auth_CreateSubscriptionToken),
@@ -1106,9 +1105,9 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressSubscriptionTokensController>(TYPES.Auth_InversifyExpressSubscriptionTokensController)
+        .bind<HomeServerSubscriptionTokensController>(TYPES.Auth_HomeServerSubscriptionTokensController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressSubscriptionTokensController(
+          new HomeServerSubscriptionTokensController(
             container.get(TYPES.Auth_CreateSubscriptionToken),
             container.get(TYPES.Auth_CreateSubscriptionToken),
             container.get(TYPES.Auth_AuthenticateSubscriptionToken),
             container.get(TYPES.Auth_AuthenticateSubscriptionToken),
             container.get(TYPES.Auth_SettingService),
             container.get(TYPES.Auth_SettingService),
@@ -1120,17 +1119,17 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressSubscriptionSettingsController>(TYPES.Auth_InversifyExpressSubscriptionSettingsController)
+        .bind<HomeServerSubscriptionSettingsController>(TYPES.Auth_HomeServerSubscriptionSettingsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressSubscriptionSettingsController(
+          new HomeServerSubscriptionSettingsController(
             container.get(TYPES.Auth_GetSetting),
             container.get(TYPES.Auth_GetSetting),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressSettingsController>(TYPES.Auth_InversifyExpressSettingsController)
+        .bind<HomeServerSettingsController>(TYPES.Auth_HomeServerSettingsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressSettingsController(
+          new HomeServerSettingsController(
             container.get(TYPES.Auth_GetSettings),
             container.get(TYPES.Auth_GetSettings),
             container.get(TYPES.Auth_GetSetting),
             container.get(TYPES.Auth_GetSetting),
             container.get(TYPES.Auth_UpdateSetting),
             container.get(TYPES.Auth_UpdateSetting),
@@ -1139,9 +1138,9 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressSessionController>(TYPES.Auth_InversifyExpressSessionController)
+        .bind<HomeServerSessionController>(TYPES.Auth_HomeServerSessionController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressSessionController(
+          new HomeServerSessionController(
             container.get(TYPES.Auth_DeleteSessionForUser),
             container.get(TYPES.Auth_DeleteSessionForUser),
             container.get(TYPES.Auth_DeletePreviousSessionsForUser),
             container.get(TYPES.Auth_DeletePreviousSessionsForUser),
             container.get(TYPES.Auth_RefreshSessionToken),
             container.get(TYPES.Auth_RefreshSessionToken),
@@ -1149,9 +1148,9 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressOfflineController>(TYPES.Auth_InversifyExpressOfflineController)
+        .bind<HomeServerOfflineController>(TYPES.Auth_HomeServerOfflineController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressOfflineController(
+          new HomeServerOfflineController(
             container.get(TYPES.Auth_GetUserFeatures),
             container.get(TYPES.Auth_GetUserFeatures),
             container.get(TYPES.Auth_GetUserOfflineSubscription),
             container.get(TYPES.Auth_GetUserOfflineSubscription),
             container.get(TYPES.Auth_CreateOfflineSubscriptionToken),
             container.get(TYPES.Auth_CreateOfflineSubscriptionToken),
@@ -1163,25 +1162,17 @@ export class ContainerConfigLoader {
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressListedController>(TYPES.Auth_InversifyExpressListedController)
+        .bind<HomeServerListedController>(TYPES.Auth_HomeServerListedController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressListedController(
+          new HomeServerListedController(
             container.get(TYPES.Auth_CreateListedAccount),
             container.get(TYPES.Auth_CreateListedAccount),
             container.get(TYPES.Auth_ControllerContainer),
             container.get(TYPES.Auth_ControllerContainer),
           ),
           ),
         )
         )
       container
       container
-        .bind<InversifyExpressInternalController>(TYPES.Auth_InversifyExpressInternalController)
+        .bind<HomeServerFeaturesController>(TYPES.Auth_HomeServerFeaturesController)
         .toConstantValue(
         .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_GetUserFeatures),
             container.get(TYPES.Auth_ControllerContainer),
             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_ProtocolVersionSelector: Symbol.for('Auth_ProtocolVersionSelector'),
   Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
   Auth_BooleanSelector: Symbol.for('Auth_BooleanSelector'),
   Auth_UserSubscriptionService: Symbol.for('Auth_UserSubscriptionService'),
   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
 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 { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 describe('InversifyExpressAdminController', () => {
 describe('InversifyExpressAdminController', () => {
   let deleteSetting: DeleteSetting
   let deleteSetting: DeleteSetting
@@ -17,7 +16,6 @@ describe('InversifyExpressAdminController', () => {
   let createOfflineSubscriptionToken: CreateOfflineSubscriptionToken
   let createOfflineSubscriptionToken: CreateOfflineSubscriptionToken
   let request: express.Request
   let request: express.Request
   let user: User
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
 
   const createController = () =>
   const createController = () =>
     new InversifyExpressAdminController(
     new InversifyExpressAdminController(
@@ -25,7 +23,6 @@ describe('InversifyExpressAdminController', () => {
       userRepository,
       userRepository,
       createSubscriptionToken,
       createSubscriptionToken,
       createOfflineSubscriptionToken,
       createOfflineSubscriptionToken,
-      controllerContainer,
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
@@ -58,9 +55,6 @@ describe('InversifyExpressAdminController', () => {
       body: {},
       body: {},
       params: {},
       params: {},
     } as jest.Mocked<express.Request>
     } as jest.Mocked<express.Request>
-
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
   })
   })
 
 
   it('should return error if missing email parameter', async () => {
   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 { Request } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpDelete,
   httpDelete,
   httpGet,
   httpGet,
@@ -12,124 +9,48 @@ import {
   results,
   results,
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
+import { HomeServerAdminController } from './HomeServer/HomeServerAdminController'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { UserRepositoryInterface } from '../../Domain/User/UserRepositoryInterface'
 import { UserRepositoryInterface } from '../../Domain/User/UserRepositoryInterface'
 
 
 @controller('/admin')
 @controller('/admin')
-export class InversifyExpressAdminController extends BaseHttpController {
+export class InversifyExpressAdminController extends HomeServerAdminController {
   constructor(
   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)
     @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')
   @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')
   @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')
   @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')
   @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')
   @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 { Request, Response } from 'express'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpGet,
   httpGet,
   httpPost,
   httpPost,
@@ -16,301 +15,65 @@ import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempt
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 import { GetUserKeyParams } from '../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
 import { GetUserKeyParams } from '../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
 import { AuthController } from '../../Controller/AuthController'
 import { AuthController } from '../../Controller/AuthController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
+import { HomeServerAuthController } from './HomeServer/HomeServerAuthController'
 
 
 @controller('/auth')
 @controller('/auth')
-export class InversifyExpressAuthController extends BaseHttpController {
+export class InversifyExpressAuthController extends HomeServerAuthController {
   constructor(
   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)
   @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)
   @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)
   @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)
   @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)
   @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)
   @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')
   @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)
   @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('/')
   @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 { Request, Response } from 'express'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpDelete,
   httpDelete,
   httpGet,
   httpGet,
@@ -10,78 +9,39 @@ import {
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { AuthenticatorsController } from '../../Controller/AuthenticatorsController'
 import { AuthenticatorsController } from '../../Controller/AuthenticatorsController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
+import { HomeServerAuthenticatorsController } from './HomeServer/HomeServerAuthenticatorsController'
 
 
 @controller('/authenticators')
 @controller('/authenticators')
-export class InversifyExpressAuthenticatorsController extends BaseHttpController {
+export class InversifyExpressAuthenticatorsController extends HomeServerAuthenticatorsController {
   constructor(
   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)
   @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)
   @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)
   @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)
   @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')
   @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 { results } from 'inversify-express-utils'
 import { User } from '../../Domain/User/User'
 import { User } from '../../Domain/User/User'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 describe('InversifyExpressFeaturesController', () => {
 describe('InversifyExpressFeaturesController', () => {
   let getUserFeatures: GetUserFeatures
   let getUserFeatures: GetUserFeatures
@@ -14,14 +13,10 @@ describe('InversifyExpressFeaturesController', () => {
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
 
-  const createController = () => new InversifyExpressFeaturesController(getUserFeatures, controllerContainer)
+  const createController = () => new InversifyExpressFeaturesController(getUserFeatures)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user = {} as jest.Mocked<User>
     user.uuid = '123'
     user.uuid = '123'
 
 

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

@@ -1,7 +1,6 @@
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpGet,
   httpGet,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9,41 +8,16 @@ import {
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerFeaturesController } from './HomeServer/HomeServerFeaturesController'
 
 
 @controller('/users/:userUuid/features')
 @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)
   @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 { InversifyExpressListedController } from './InversifyExpressListedController'
 import { User } from '../../Domain/User/User'
 import { User } from '../../Domain/User/User'
 import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
 import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 describe('InversifyExpressListedController', () => {
 describe('InversifyExpressListedController', () => {
   let createListedAccount: CreateListedAccount
   let createListedAccount: CreateListedAccount
@@ -14,14 +13,10 @@ describe('InversifyExpressListedController', () => {
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
 
-  const createController = () => new InversifyExpressListedController(createListedAccount, controllerContainer)
+  const createController = () => new InversifyExpressListedController(createListedAccount)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user = {} as jest.Mocked<User>
     user.uuid = '123'
     user.uuid = '123'
 
 

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

@@ -1,44 +1,19 @@
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // 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 { Request, Response } from 'express'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { CreateListedAccount } from '../../Domain/UseCase/CreateListedAccount/CreateListedAccount'
 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')
 @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)
   @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 { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/security'
 import { SubscriptionName } from '@standardnotes/common'
 import { SubscriptionName } from '@standardnotes/common'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 describe('InversifyExpressOfflineController', () => {
 describe('InversifyExpressOfflineController', () => {
   let getUserFeatures: GetUserFeatures
   let getUserFeatures: GetUserFeatures
@@ -29,8 +28,6 @@ describe('InversifyExpressOfflineController', () => {
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
 
 
-  let controllerContainer: ControllerContainerInterface
-
   const createController = () =>
   const createController = () =>
     new InversifyExpressOfflineController(
     new InversifyExpressOfflineController(
       getUserFeatures,
       getUserFeatures,
@@ -40,13 +37,9 @@ describe('InversifyExpressOfflineController', () => {
       tokenEncoder,
       tokenEncoder,
       jwtTTL,
       jwtTTL,
       logger,
       logger,
-      controllerContainer,
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user = {} as jest.Mocked<User>
     user.uuid = '123'
     user.uuid = '123'
 
 

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

@@ -1,7 +1,6 @@
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpGet,
   httpGet,
   httpPost,
   httpPost,
@@ -10,132 +9,54 @@ import {
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
 import { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/security'
 import { OfflineUserTokenData, TokenEncoderInterface } from '@standardnotes/security'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { AuthenticateOfflineSubscriptionToken } from '../../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
 import { AuthenticateOfflineSubscriptionToken } from '../../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { CreateOfflineSubscriptionToken } from '../../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
 import { GetUserFeatures } from '../../Domain/UseCase/GetUserFeatures/GetUserFeatures'
 import { GetUserOfflineSubscription } from '../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
 import { GetUserOfflineSubscription } from '../../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
+import { HomeServerOfflineController } from './HomeServer/HomeServerOfflineController'
 
 
 @controller('/offline')
 @controller('/offline')
-export class InversifyExpressOfflineController extends BaseHttpController {
+export class InversifyExpressOfflineController extends HomeServerOfflineController {
   constructor(
   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)
     @inject(TYPES.Auth_CreateOfflineSubscriptionToken)
-    private createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
+    override createOfflineSubscriptionToken: CreateOfflineSubscriptionToken,
     @inject(TYPES.Auth_AuthenticateOfflineSubscriptionToken)
     @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)
   @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')
   @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')
   @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)
   @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 { InversifyExpressSessionController } from './InversifyExpressSessionController'
 import { results } from 'inversify-express-utils'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
 import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
 import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
 import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
 import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
 import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
@@ -15,20 +14,11 @@ describe('InversifyExpressSessionController', () => {
   let refreshSessionToken: RefreshSessionToken
   let refreshSessionToken: RefreshSessionToken
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
-  let controllerContainer: ControllerContainerInterface
 
 
   const createController = () =>
   const createController = () =>
-    new InversifyExpressSessionController(
-      deleteSessionForUser,
-      deletePreviousSessionsForUser,
-      refreshSessionToken,
-      controllerContainer,
-    )
+    new InversifyExpressSessionController(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     deleteSessionForUser = {} as jest.Mocked<DeleteSessionForUser>
     deleteSessionForUser = {} as jest.Mocked<DeleteSessionForUser>
     deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: true })
     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 { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpDelete,
   httpDelete,
   httpPost,
   httpPost,
@@ -13,152 +11,37 @@ import TYPES from '../../Bootstrap/Types'
 import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
 import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
 import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
 import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
 import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
 import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSessionController } from './HomeServer/HomeServerSessionController'
 
 
 @controller('/session')
 @controller('/session')
-export class InversifyExpressSessionController extends BaseHttpController {
+export class InversifyExpressSessionController extends HomeServerSessionController {
   constructor(
   constructor(
-    @inject(TYPES.Auth_DeleteSessionForUser) private deleteSessionForUser: DeleteSessionForUser,
+    @inject(TYPES.Auth_DeleteSessionForUser) override deleteSessionForUser: DeleteSessionForUser,
     @inject(TYPES.Auth_DeletePreviousSessionsForUser)
     @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)
   @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)
   @httpDelete('/all', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
-  async deleteAllSessions(
+  override async deleteAllSessions(
     _request: Request,
     _request: Request,
     response: Response,
     response: Response,
   ): Promise<results.JsonResult | results.StatusCodeResult> {
   ): 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')
   @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 { InversifyExpressSessionsController } from './InversifyExpressSessionsController'
 import { results } from 'inversify-express-utils'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { User } from '@standardnotes/responses'
 import { User } from '@standardnotes/responses'
 
 
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
@@ -22,7 +21,6 @@ describe('InversifyExpressSessionsController', () => {
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
   let createCrossServiceToken: CreateCrossServiceToken
   let createCrossServiceToken: CreateCrossServiceToken
-  let controllerContainer: ControllerContainerInterface
 
 
   const createController = () =>
   const createController = () =>
     new InversifyExpressSessionsController(
     new InversifyExpressSessionsController(
@@ -30,13 +28,9 @@ describe('InversifyExpressSessionsController', () => {
       authenticateRequest,
       authenticateRequest,
       sessionProjector,
       sessionProjector,
       createCrossServiceToken,
       createCrossServiceToken,
-      controllerContainer,
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     session = {} as jest.Mocked<Session>
     session = {} as jest.Mocked<Session>
 
 
     user = {} as jest.Mocked<User>
     user = {} as jest.Mocked<User>

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

@@ -1,85 +1,39 @@
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpGet,
   httpGet,
   httpPost,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
   results,
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
 import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
 import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
 import { GetActiveSessionsForUser } from '../../Domain/UseCase/GetActiveSessionsForUser'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { SessionProjector } from '../../Projection/SessionProjector'
-import { User } from '../../Domain/User/User'
 import { Session } from '../../Domain/Session/Session'
 import { Session } from '../../Domain/Session/Session'
+import { HomeServerSessionsController } from './HomeServer/HomeServerSessionsController'
 
 
 @controller('/sessions')
 @controller('/sessions')
-export class InversifyExpressSessionsController extends BaseHttpController {
+export class InversifyExpressSessionsController extends HomeServerSessionsController {
   constructor(
   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')
   @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)
   @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 { InversifyExpressSettingsController } from './InversifyExpressSettingsController'
 import { results } from 'inversify-express-utils'
 import { results } from 'inversify-express-utils'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion'
 import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
@@ -21,15 +20,11 @@ describe('InversifyExpressSettingsController', () => {
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
 
   const createController = () =>
   const createController = () =>
-    new InversifyExpressSettingsController(getSettings, getSetting, updateSetting, deleteSetting, controllerContainer)
+    new InversifyExpressSettingsController(getSettings, getSetting, updateSetting, deleteSetting)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     deleteSetting = {} as jest.Mocked<DeleteSetting>
     deleteSetting = {} as jest.Mocked<DeleteSetting>
     deleteSetting.execute = jest.fn().mockReturnValue({ success: true })
     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 { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpDelete,
   httpDelete,
   httpGet,
   httpGet,
@@ -11,155 +9,43 @@ import {
   results,
   results,
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
-import { EncryptionVersion } from '../../Domain/Encryption/EncryptionVersion'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { DeleteSetting } from '../../Domain/UseCase/DeleteSetting/DeleteSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
 import { GetSettings } from '../../Domain/UseCase/GetSettings/GetSettings'
 import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
 import { UpdateSetting } from '../../Domain/UseCase/UpdateSetting/UpdateSetting'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSettingsController } from './HomeServer/HomeServerSettingsController'
 
 
 @controller('/users/:userUuid')
 @controller('/users/:userUuid')
-export class InversifyExpressSettingsController extends BaseHttpController {
+export class InversifyExpressSettingsController extends HomeServerSettingsController {
   constructor(
   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)
   @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)
   @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)
   @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)
   @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 { Request, Response } from 'express'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpDelete,
   httpDelete,
   httpGet,
   httpGet,
@@ -10,79 +7,43 @@ import {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
   results,
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
+import { inject } from 'inversify'
+
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
 import { SubscriptionInvitesController } from '../../Controller/SubscriptionInvitesController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
-import { inject } from 'inversify'
+import { HomeServerSubscriptionInvitesController } from './HomeServer/HomeServerSubscriptionInvitesController'
 
 
 @controller('/subscription-invites')
 @controller('/subscription-invites')
-export class InversifyExpressSubscriptionInvitesController extends BaseHttpController {
+export class InversifyExpressSubscriptionInvitesController extends HomeServerSubscriptionInvitesController {
   constructor(
   constructor(
     @inject(TYPES.Auth_SubscriptionInvitesController)
     @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)
   @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')
   @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)
   @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)
   @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)
   @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 { results } from 'inversify-express-utils'
 import { InversifyExpressSubscriptionSettingsController } from './InversifyExpressSubscriptionSettingsController'
 import { InversifyExpressSubscriptionSettingsController } from './InversifyExpressSubscriptionSettingsController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { User } from '../../Domain/User/User'
 import { User } from '../../Domain/User/User'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 
 
@@ -14,14 +13,10 @@ describe('InversifyExpressSubscriptionSettingsController', () => {
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
 
-  const createController = () => new InversifyExpressSubscriptionSettingsController(getSetting, controllerContainer)
+  const createController = () => new InversifyExpressSubscriptionSettingsController(getSetting)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user = {} as jest.Mocked<User>
     user.uuid = '123'
     user.uuid = '123'
 
 

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

@@ -1,7 +1,6 @@
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpGet,
   httpGet,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -9,30 +8,16 @@ import {
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
 import { GetSetting } from '../../Domain/UseCase/GetSetting/GetSetting'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSubscriptionSettingsController } from './HomeServer/HomeServerSubscriptionSettingsController'
 
 
 @controller('/users/:userUuid')
 @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)
   @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 { InversifyExpressSubscriptionTokensController } from './InversifyExpressSubscriptionTokensController'
 import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
 import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+
 import { Setting } from '../../Domain/Setting/Setting'
 import { Setting } from '../../Domain/Setting/Setting'
 import { SettingServiceInterface } from '../../Domain/Setting/SettingServiceInterface'
 import { SettingServiceInterface } from '../../Domain/Setting/SettingServiceInterface'
 import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken'
 import { AuthenticateSubscriptionToken } from '../../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken'
@@ -30,8 +30,6 @@ describe('InversifyExpressSubscriptionTokensController', () => {
   let user: User
   let user: User
   let role: Role
   let role: Role
 
 
-  let controllerContainer: ControllerContainerInterface
-
   const createController = () =>
   const createController = () =>
     new InversifyExpressSubscriptionTokensController(
     new InversifyExpressSubscriptionTokensController(
       createSubscriptionToken,
       createSubscriptionToken,
@@ -41,13 +39,9 @@ describe('InversifyExpressSubscriptionTokensController', () => {
       roleProjector,
       roleProjector,
       tokenEncoder,
       tokenEncoder,
       jwtTTL,
       jwtTTL,
-      controllerContainer,
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     user = {} as jest.Mocked<User>
     user = {} as jest.Mocked<User>
     user.uuid = '123'
     user.uuid = '123'
     user.roles = Promise.resolve([role])
     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 { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
-import { ErrorTag } from '@standardnotes/responses'
-import { SettingName } from '@standardnotes/settings'
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpPost,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // 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 { CreateSubscriptionToken } from '../../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
 import { User } from '../../Domain/User/User'
 import { User } from '../../Domain/User/User'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
+import { HomeServerSubscriptionTokensController } from './HomeServer/HomeServerSubscriptionTokensController'
 
 
 @controller('/subscription-tokens')
 @controller('/subscription-tokens')
-export class InversifyExpressSubscriptionTokensController extends BaseHttpController {
+export class InversifyExpressSubscriptionTokensController extends HomeServerSubscriptionTokensController {
   constructor(
   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)
   @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')
   @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 { 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 TYPES from '../../Bootstrap/Types'
 import { UserRequestsController } from '../../Controller/UserRequestsController'
 import { UserRequestsController } from '../../Controller/UserRequestsController'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
+import { HomeServerUserRequestsController } from './HomeServer/HomeServerUserRequestsController'
 
 
 @controller('/users/:userUuid/requests')
 @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)
   @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 { InversifyExpressUsersController } from './InversifyExpressUsersController'
 import { results } from 'inversify-express-utils'
 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 { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
 import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
 import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
 import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
 import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
@@ -28,7 +28,6 @@ describe('InversifyExpressUsersController', () => {
   let request: express.Request
   let request: express.Request
   let response: express.Response
   let response: express.Response
   let user: User
   let user: User
-  let controllerContainer: ControllerContainerInterface
 
 
   const createController = () =>
   const createController = () =>
     new InversifyExpressUsersController(
     new InversifyExpressUsersController(
@@ -39,13 +38,9 @@ describe('InversifyExpressUsersController', () => {
       clearLoginAttempts,
       clearLoginAttempts,
       increaseLoginAttempts,
       increaseLoginAttempts,
       changeCredentials,
       changeCredentials,
-      controllerContainer,
     )
     )
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     updateUser = {} as jest.Mocked<UpdateUser>
     updateUser = {} as jest.Mocked<UpdateUser>
     updateUser.execute = jest.fn()
     updateUser.execute = jest.fn()
 
 

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

@@ -1,8 +1,6 @@
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
-import { ErrorTag } from '@standardnotes/responses'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpDelete,
   httpDelete,
   httpGet,
   httpGet,
@@ -19,232 +17,52 @@ import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/Ge
 import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
 import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
 import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
 import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
 import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
 import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
-import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
+import { HomeServerUsersController } from './HomeServer/HomeServerUsersController'
 
 
 @controller('/users')
 @controller('/users')
-export class InversifyExpressUsersController extends BaseHttpController {
+export class InversifyExpressUsersController extends HomeServerUsersController {
   constructor(
   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)
   @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')
   @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')
   @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)
   @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)
   @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 { results } from 'inversify-express-utils'
 import { InversifyExpressValetTokenController } from './InversifyExpressValetTokenController'
 import { InversifyExpressValetTokenController } from './InversifyExpressValetTokenController'
 import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
 import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 describe('InversifyExpressValetTokenController', () => {
 describe('InversifyExpressValetTokenController', () => {
   let createValetToken: CreateValetToken
   let createValetToken: CreateValetToken
   let request: Request
   let request: Request
   let response: Response
   let response: Response
-  let controllerContainer: ControllerContainerInterface
 
 
-  const createController = () => new InversifyExpressValetTokenController(createValetToken, controllerContainer)
+  const createController = () => new InversifyExpressValetTokenController(createValetToken)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     createValetToken = {} as jest.Mocked<CreateValetToken>
     createValetToken = {} as jest.Mocked<CreateValetToken>
     createValetToken.execute = jest.fn().mockReturnValue({ success: true, valetToken: 'foobar' })
     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 { inject } from 'inversify'
 import { Request, Response } from 'express'
 import { Request, Response } from 'express'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpPost,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
   results,
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
-import { CreateValetTokenPayload, ErrorTag } from '@standardnotes/responses'
-import { ValetTokenOperation } from '@standardnotes/security'
-import { ControllerContainerInterface, Uuid } from '@standardnotes/domain-core'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
 import { CreateValetToken } from '../../Domain/UseCase/CreateValetToken/CreateValetToken'
+import { HomeServerValetTokenController } from './HomeServer/HomeServerValetTokenController'
 
 
 @controller('/valet-tokens', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
 @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('/')
   @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 { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
 import { Request } from 'express'
 import { Request } from 'express'
 import {
 import {
-  BaseHttpController,
   controller,
   controller,
   httpPost,
   httpPost,
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   results,
   results,
 } from 'inversify-express-utils'
 } from 'inversify-express-utils'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
 import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
+import { HomeServerWebSocketsController } from './HomeServer/HomeServerWebSocketsController'
 
 
 @controller('/sockets')
 @controller('/sockets')
-export class InversifyExpressWebSocketsController extends BaseHttpController {
+export class InversifyExpressWebSocketsController extends HomeServerWebSocketsController {
   constructor(
   constructor(
-    @inject(TYPES.Auth_CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
+    @inject(TYPES.Auth_CreateCrossServiceToken) override createCrossServiceToken: CreateCrossServiceToken,
     @inject(TYPES.Auth_WebSocketConnectionTokenDecoder)
     @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')
   @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',
     Auth: 'Auth',
     SyncingServer: 'SyncingServer',
     SyncingServer: 'SyncingServer',
     Revisions: 'Revisions',
     Revisions: 'Revisions',
+    Files: 'Files',
   }
   }
 
 
   get value(): string {
   get value(): string {

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

@@ -4,8 +4,8 @@ import 'newrelic'
 
 
 import * as busboy from 'connect-busboy'
 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 helmet from 'helmet'
 import * as cors from 'cors'
 import * as cors from 'cors'
@@ -28,7 +28,7 @@ void container.load().then((container) => {
 
 
   server.setConfig((app) => {
   server.setConfig((app) => {
     app.use((_request: Request, response: Response, next: NextFunction) => {
     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()
       next()
     })
     })
     app.use(
     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) => {
   server.setErrorConfig((app) => {
     app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
     app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {

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

@@ -18,11 +18,13 @@ void container.load().then((container) => {
   const env: Env = new Env()
   const env: Env = new Env()
   env.load()
   env.load()
 
 
-  const logger: Logger = container.get(TYPES.Logger)
+  const logger: Logger = container.get(TYPES.Files_Logger)
 
 
   logger.info('Starting worker...')
   logger.info('Starting worker...')
 
 
-  const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
+  const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(
+    TYPES.Files_DomainEventSubscriberFactory,
+  )
   subscriberFactory.create().start()
   subscriberFactory.create().start()
 
 
   setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
   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",
     "setup:env": "cp .env.sample .env",
     "build": "tsc --build",
     "build": "tsc --build",
     "lint": "eslint . --ext .ts",
     "lint": "eslint . --ext .ts",
+    "lint:fix": "eslint . --fix --ext .ts",
     "pretest": "yarn lint && yarn build",
     "pretest": "yarn lint && yarn build",
     "test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
     "test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
     "start": "yarn node dist/bin/server.js",
     "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 { Env } from './Env'
 import TYPES from './Types'
 import TYPES from './Types'
 import { UploadFileChunk } from '../Domain/UseCase/UploadFileChunk/UploadFileChunk'
 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 { TokenDecoder, TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
 import { Timer, TimerInterface } from '@standardnotes/time'
 import { Timer, TimerInterface } from '@standardnotes/time'
 import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
 import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryInterface'
 import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
 import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
 import {
 import {
+  DirectCallDomainEventPublisher,
+  DirectCallEventMessageHandler,
   SNSDomainEventPublisher,
   SNSDomainEventPublisher,
   SQSDomainEventSubscriberFactory,
   SQSDomainEventSubscriberFactory,
   SQSEventMessageHandler,
   SQSEventMessageHandler,
@@ -38,6 +40,7 @@ import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
 import {
 import {
   DomainEventHandlerInterface,
   DomainEventHandlerInterface,
   DomainEventMessageHandlerInterface,
   DomainEventMessageHandlerInterface,
+  DomainEventPublisherInterface,
   DomainEventSubscriberFactoryInterface,
   DomainEventSubscriberFactoryInterface,
 } from '@standardnotes/domain-events'
 } from '@standardnotes/domain-events'
 import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
 import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
@@ -46,7 +49,10 @@ import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Hand
 import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
 import { InMemoryUploadRepository } from '../Infra/InMemory/InMemoryUploadRepository'
 
 
 export class ContainerConfigLoader {
 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()
     const env: Env = new Env()
     env.load()
     env.load()
 
 
@@ -55,32 +61,88 @@ export class ContainerConfigLoader {
     const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
     const isConfiguredForHomeServer = env.get('CACHE_TYPE') === 'memory'
 
 
     const logger = this.createLogger({ env })
     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 {
     } 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)) {
     if (env.get('S3_AWS_REGION', true) || env.get('S3_ENDPOINT', true)) {
       const s3Opts: S3ClientConfig = {
       const s3Opts: S3ClientConfig = {
@@ -93,115 +155,83 @@ export class ContainerConfigLoader {
         s3Opts.endpoint = env.get('S3_ENDPOINT', true)
         s3Opts.endpoint = env.get('S3_ENDPOINT', true)
       }
       }
       const s3Client = new S3Client(s3Opts)
       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 {
     } else {
-      container.bind<FileDownloaderInterface>(TYPES.FileDownloader).to(FSFileDownloader)
+      container.bind<FileDownloaderInterface>(TYPES.Files_FileDownloader).to(FSFileDownloader)
       container
       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
     // 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
     // middleware
-    container.bind<ValetTokenAuthMiddleware>(TYPES.ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
+    container.bind<ValetTokenAuthMiddleware>(TYPES.Files_ValetTokenAuthMiddleware).to(ValetTokenAuthMiddleware)
 
 
     // services
     // services
     container
     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
     // Handlers
     container
     container
-      .bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
+      .bind<AccountDeletionRequestedEventHandler>(TYPES.Files_AccountDeletionRequestedEventHandler)
       .to(AccountDeletionRequestedEventHandler)
       .to(AccountDeletionRequestedEventHandler)
     container
     container
-      .bind<SharedSubscriptionInvitationCanceledEventHandler>(TYPES.SharedSubscriptionInvitationCanceledEventHandler)
+      .bind<SharedSubscriptionInvitationCanceledEventHandler>(
+        TYPES.Files_SharedSubscriptionInvitationCanceledEventHandler,
+      )
       .to(SharedSubscriptionInvitationCanceledEventHandler)
       .to(SharedSubscriptionInvitationCanceledEventHandler)
 
 
     const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
     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',
         '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
     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 = {
 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
   // 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
   // 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
   // repositories
-  UploadRepository: Symbol.for('UploadRepository'),
+  Files_UploadRepository: Symbol.for('Files_UploadRepository'),
 
 
   // middleware
   // middleware
-  ValetTokenAuthMiddleware: Symbol.for('ValetTokenAuthMiddleware'),
+  Files_ValetTokenAuthMiddleware: Symbol.for('Files_ValetTokenAuthMiddleware'),
 
 
   // env vars
   // 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
   // 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
 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()
 @injectable()
 export class DomainEventFactory implements DomainEventFactoryInterface {
 export class DomainEventFactory implements DomainEventFactoryInterface {
-  constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
+  constructor(@inject(TYPES.Files_Timer) private timer: TimerInterface) {}
 
 
   createFileRemovedEvent(payload: {
   createFileRemovedEvent(payload: {
     userUuid: string
     userUuid: string

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

@@ -12,9 +12,9 @@ import { MarkFilesToBeRemoved } from '../UseCase/MarkFilesToBeRemoved/MarkFilesT
 @injectable()
 @injectable()
 export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
 export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class SharedSubscriptionInvitationCanceledEventHandler implements DomainEventHandlerInterface {
 export class SharedSubscriptionInvitationCanceledEventHandler implements DomainEventHandlerInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class CreateUploadSession implements UseCaseInterface {
 export class CreateUploadSession implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class FinishUploadSession implements UseCaseInterface {
 export class FinishUploadSession implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class GetFileMetadata implements UseCaseInterface {
 export class GetFileMetadata implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class MarkFilesToBeRemoved implements UseCaseInterface {
 export class MarkFilesToBeRemoved implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class RemoveFile implements UseCaseInterface {
 export class RemoveFile implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class StreamDownloadFile implements UseCaseInterface {
 export class StreamDownloadFile implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class UploadFileChunk implements UseCaseInterface {
 export class UploadFileChunk implements UseCaseInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class FSFileDownloader implements FileDownloaderInterface {
 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> {
   async getFileSize(filePath: string): Promise<number> {
     return (await promises.stat(`${this.fileUploadPath}/${filePath}`)).size
     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()
 @injectable()
 export class FSFileRemover implements FileRemoverInterface {
 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>> {
   async markFilesToBeRemoved(userUuid: string): Promise<Array<RemovedFileDescription>> {
     await promises.rmdir(`${this.fileUploadPath}/${userUuid}`)
     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>>
   private inMemoryChunks: Map<string, Map<number, Uint8Array>>
 
 
   constructor(
   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>>()
     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 '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 { Request, Response } from 'express'
 import { Writable, Readable } from 'stream'
 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 { 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 { ValetTokenOperation } from '@standardnotes/security'
 import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
 import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
 
 
-describe('FilesController', () => {
+describe('InversifyExpressFilesController', () => {
   let uploadFileChunk: UploadFileChunk
   let uploadFileChunk: UploadFileChunk
   let createUploadSession: CreateUploadSession
   let createUploadSession: CreateUploadSession
   let finishUploadSession: FinishUploadSession
   let finishUploadSession: FinishUploadSession
@@ -27,7 +27,7 @@ describe('FilesController', () => {
   const maxChunkBytes = 100_000
   const maxChunkBytes = 100_000
 
 
   const createController = () =>
   const createController = () =>
-    new FilesController(
+    new InversifyExpressFilesController(
       uploadFileChunk,
       uploadFileChunk,
       createUploadSession,
       createUploadSession,
       finishUploadSession,
       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 { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { inject } from 'inversify'
 import { Writable } from 'stream'
 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'
 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(
   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()
     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'
 import { controller, httpGet } from 'inversify-express-utils'
 
 
 @controller('/healthcheck')
 @controller('/healthcheck')
-export class HealthCheckController {
+export class InversifyExpressHealthCheckController {
   @httpGet('/')
   @httpGet('/')
   public async get(): Promise<string> {
   public async get(): Promise<string> {
     return 'OK'
     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 { inject, injectable } from 'inversify'
 import { BaseMiddleware } from 'inversify-express-utils'
 import { BaseMiddleware } from 'inversify-express-utils'
 import { Logger } from 'winston'
 import { Logger } from 'winston'
-import TYPES from '../Bootstrap/Types'
+import TYPES from '../../../Bootstrap/Types'
 
 
 @injectable()
 @injectable()
 export class ValetTokenAuthMiddleware extends BaseMiddleware {
 export class ValetTokenAuthMiddleware extends BaseMiddleware {
   constructor(
   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()
     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_CHUNKS_PREFIX = 'upload-chunks'
   private readonly UPLOAD_SESSION_DEFAULT_TTL = 7200
   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> {
   async storeUploadSession(filePath: string, uploadId: string): Promise<void> {
     await this.redisClient.setex(`${this.UPLOAD_SESSION_PREFIX}:${filePath}`, this.UPLOAD_SESSION_DEFAULT_TTL, uploadId)
     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()
 @injectable()
 export class S3FileDownloader implements FileDownloaderInterface {
 export class S3FileDownloader implements FileDownloaderInterface {
   constructor(
   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> {
   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()
 @injectable()
 export class S3FileRemover implements FileRemoverInterface {
 export class S3FileRemover implements FileRemoverInterface {
   constructor(
   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>> {
   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()
 @injectable()
 export class S3FileUploader implements FileUploaderInterface {
 export class S3FileUploader implements FileUploaderInterface {
   constructor(
   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> {
   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=
 AUTH_JWT_SECRET=
 ENCRYPTION_SERVER_KEY=
 ENCRYPTION_SERVER_KEY=
 PSEUDO_KEY_PARAMS_KEY=
 PSEUDO_KEY_PARAMS_KEY=
+VALET_TOKEN_SECRET=
 
 
 FILES_SERVER_URL=
 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 { ControllerContainer, ServiceContainer } from '@standardnotes/domain-core'
 import { Service as ApiGatewayService, TYPES as ApiGatewayTYPES } from '@standardnotes/api-gateway'
 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 { DirectCallDomainEventPublisher } from '@standardnotes/domain-events-infra'
 import { Service as AuthService } from '@standardnotes/auth-server'
 import { Service as AuthService } from '@standardnotes/auth-server'
 import { Service as SyncingService } from '@standardnotes/syncing-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 authService = new AuthService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
   const syncingService = new SyncingService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
   const syncingService = new SyncingService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
   const revisionsService = new RevisionsService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
   const revisionsService = new RevisionsService(serviceContainer, controllerContainer, directCallDomainEventPublisher)
+  const filesService = new FilesService(serviceContainer, directCallDomainEventPublisher)
 
 
   const container = Container.merge(
   const container = Container.merge(
     (await apiGatewayService.getContainer()) as Container,
     (await apiGatewayService.getContainer()) as Container,
     (await authService.getContainer()) as Container,
     (await authService.getContainer()) as Container,
     (await syncingService.getContainer()) as Container,
     (await syncingService.getContainer()) as Container,
     (await revisionsService.getContainer()) as Container,
     (await revisionsService.getContainer()) as Container,
+    (await filesService.getContainer()) as Container,
   )
   )
 
 
   const env: Env = new Env()
   const env: Env = new Env()

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

@@ -22,6 +22,7 @@
     "@standardnotes/auth-server": "workspace:^",
     "@standardnotes/auth-server": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events-infra": "workspace:^",
     "@standardnotes/domain-events-infra": "workspace:^",
+    "@standardnotes/files-server": "workspace:^",
     "@standardnotes/revisions-server": "workspace:^",
     "@standardnotes/revisions-server": "workspace:^",
     "@standardnotes/syncing-server": "workspace:^",
     "@standardnotes/syncing-server": "workspace:^",
     "cors": "2.8.5",
     "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 { FSDumpRepository } from '../Infra/FS/FSDumpRepository'
 import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
 import { S3DumpRepository } from '../Infra/S3/S3ItemDumpRepository'
 import { RevisionItemStringMapper } from '../Mapping/RevisionItemStringMapper'
 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
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
 const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -337,9 +337,9 @@ export class ContainerConfigLoader {
     // Inversify Controllers
     // Inversify Controllers
     if (isConfiguredForHomeServer) {
     if (isConfiguredForHomeServer) {
       container
       container
-        .bind<InversifyExpressRevisionsController>(TYPES.Revisions_InversifyExpressRevisionsController)
+        .bind<HomeServerRevisionsController>(TYPES.Revisions_HomeServerRevisionsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressRevisionsController(
+          new HomeServerRevisionsController(
             container.get(TYPES.Revisions_RevisionsController),
             container.get(TYPES.Revisions_RevisionsController),
             container.get(TYPES.Revisions_ControllerContainer),
             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_DomainEventMessageHandler: Symbol.for('Revisions_DomainEventMessageHandler'),
   Revisions_Timer: Symbol.for('Revisions_Timer'),
   Revisions_Timer: Symbol.for('Revisions_Timer'),
   // Inversify Express Controllers
   // Inversify Express Controllers
-  Revisions_InversifyExpressRevisionsController: Symbol.for('Revisions_InversifyExpressRevisionsController'),
+  Revisions_HomeServerRevisionsController: Symbol.for('Revisions_HomeServerRevisionsController'),
 }
 }
 
 
 export default TYPES
 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 { 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 { inject } from 'inversify'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 import TYPES from '../../Bootstrap/Types'
 import TYPES from '../../Bootstrap/Types'
 import { RevisionsController } from '../../Controller/RevisionsController'
 import { RevisionsController } from '../../Controller/RevisionsController'
+import { HomeServerRevisionsController } from './HomeServer/HomeServerRevisionsController'
 
 
 @controller('/items/:itemUuid/revisions', TYPES.Revisions_ApiGatewayAuthMiddleware)
 @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('/')
   @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')
   @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')
   @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 { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
 import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
 import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
 import { ControllerContainer, ControllerContainerInterface } from '@standardnotes/domain-core'
 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
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const newrelicFormatter = require('@newrelic/winston-enricher')
 const newrelicFormatter = require('@newrelic/winston-enricher')
 
 
@@ -499,9 +499,9 @@ export class ContainerConfigLoader {
 
 
     if (isConfiguredForHomeServer) {
     if (isConfiguredForHomeServer) {
       container
       container
-        .bind<InversifyExpressItemsController>(TYPES.Sync_InversifyExpressItemsController)
+        .bind<HomeServerItemsController>(TYPES.Sync_HomeServerItemsController)
         .toConstantValue(
         .toConstantValue(
-          new InversifyExpressItemsController(
+          new HomeServerItemsController(
             container.get(TYPES.Sync_SyncItems),
             container.get(TYPES.Sync_SyncItems),
             container.get(TYPES.Sync_CheckIntegrity),
             container.get(TYPES.Sync_CheckIntegrity),
             container.get(TYPES.Sync_GetItem),
             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_ItemFactory: Symbol.for('Sync_ItemFactory'),
   Sync_ItemTransferCalculator: Symbol.for('Sync_ItemTransferCalculator'),
   Sync_ItemTransferCalculator: Symbol.for('Sync_ItemTransferCalculator'),
   Sync_ControllerContainer: Symbol.for('Sync_ControllerContainer'),
   Sync_ControllerContainer: Symbol.for('Sync_ControllerContainer'),
-  Sync_InversifyExpressItemsController: Symbol.for('Sync_InversifyExpressItemsController'),
+  Sync_HomeServerItemsController: Symbol.for('Sync_HomeServerItemsController'),
 }
 }
 
 
 export default TYPES
 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 { CheckIntegrity } from '../../Domain/UseCase/CheckIntegrity/CheckIntegrity'
 import { GetItem } from '../../Domain/UseCase/GetItem/GetItem'
 import { GetItem } from '../../Domain/UseCase/GetItem/GetItem'
 import { SyncItems } from '../../Domain/UseCase/SyncItems'
 import { SyncItems } from '../../Domain/UseCase/SyncItems'
-import { ControllerContainerInterface } from '@standardnotes/domain-core'
 
 
 describe('InversifyExpressItemsController', () => {
 describe('InversifyExpressItemsController', () => {
   let syncItems: SyncItems
   let syncItems: SyncItems
@@ -27,22 +26,11 @@ describe('InversifyExpressItemsController', () => {
   let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
   let syncResponceFactoryResolver: SyncResponseFactoryResolverInterface
   let syncResponseFactory: SyncResponseFactoryInterface
   let syncResponseFactory: SyncResponseFactoryInterface
   let syncResponse: SyncResponse20200115
   let syncResponse: SyncResponse20200115
-  let controllerContainer: ControllerContainerInterface
 
 
   const createController = () =>
   const createController = () =>
-    new InversifyExpressItemsController(
-      syncItems,
-      checkIntegrity,
-      getItem,
-      itemProjector,
-      syncResponceFactoryResolver,
-      controllerContainer,
-    )
+    new InversifyExpressItemsController(syncItems, checkIntegrity, getItem, itemProjector, syncResponceFactoryResolver)
 
 
   beforeEach(() => {
   beforeEach(() => {
-    controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
-    controllerContainer.register = jest.fn()
-
     itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
     itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
     itemProjector.projectFull = jest.fn().mockReturnValue({ foo: 'bar' })
     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 { Request, Response } from 'express'
 import { inject } from 'inversify'
 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 TYPES from '../../Bootstrap/Types'
 import { Item } from '../../Domain/Item/Item'
 import { Item } from '../../Domain/Item/Item'
@@ -10,84 +10,36 @@ import { GetItem } from '../../Domain/UseCase/GetItem/GetItem'
 import { SyncItems } from '../../Domain/UseCase/SyncItems'
 import { SyncItems } from '../../Domain/UseCase/SyncItems'
 import { ItemProjection } from '../../Projection/ItemProjection'
 import { ItemProjection } from '../../Projection/ItemProjection'
 import { ProjectorInterface } from '../../Projection/ProjectorInterface'
 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)
 @controller('/items', TYPES.Sync_AuthMiddleware)
-export class InversifyExpressItemsController extends BaseHttpController {
+export class InversifyExpressItemsController extends HomeServerItemsController {
   constructor(
   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)
     @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')
   @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')
   @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')
   @httpGet('/:uuid')
-  public async getSingleItem(
+  override async getSingleItem(
     request: Request,
     request: Request,
     response: Response,
     response: Response,
   ): Promise<results.NotFoundResult | results.JsonResult> {
   ): 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
   languageName: node
   linkType: hard
   linkType: hard
 
 
-"@standardnotes/files-server@workspace:packages/files":
+"@standardnotes/files-server@workspace:^, @standardnotes/files-server@workspace:packages/files":
   version: 0.0.0-use.local
   version: 0.0.0-use.local
   resolution: "@standardnotes/files-server@workspace:packages/files"
   resolution: "@standardnotes/files-server@workspace:packages/files"
   dependencies:
   dependencies:
@@ -4249,6 +4249,7 @@ __metadata:
     "@standardnotes/auth-server": "workspace:^"
     "@standardnotes/auth-server": "workspace:^"
     "@standardnotes/domain-core": "workspace:^"
     "@standardnotes/domain-core": "workspace:^"
     "@standardnotes/domain-events-infra": "workspace:^"
     "@standardnotes/domain-events-infra": "workspace:^"
+    "@standardnotes/files-server": "workspace:^"
     "@standardnotes/revisions-server": "workspace:^"
     "@standardnotes/revisions-server": "workspace:^"
     "@standardnotes/syncing-server": "workspace:^"
     "@standardnotes/syncing-server": "workspace:^"
     "@types/cors": "npm:^2.8.9"
     "@types/cors": "npm:^2.8.9"