Compare commits

..

8 Commits

Author SHA1 Message Date
standardci
f9183b4c62 chore(release): publish new version
- @standardnotes/analytics@2.22.5
 - @standardnotes/api-gateway@1.59.0
 - @standardnotes/auth-server@1.112.0
 - @standardnotes/domain-core@1.16.0
 - @standardnotes/files-server@1.13.0
 - @standardnotes/home-server@1.6.0
 - @standardnotes/revisions-server@1.17.0
 - @standardnotes/scheduler-server@1.18.5
 - @standardnotes/settings@1.21.5
 - @standardnotes/syncing-server@1.38.0
 - @standardnotes/websockets-server@1.7.5
2023-05-29 12:59:40 +00:00
Karol Sójko
c7d575a0ff 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
2023-05-29 14:45:49 +02:00
Karol Sójko
a575e62519 fix: waiting for home server to start in ci 2023-05-29 14:05:15 +02:00
Karol Sójko
3761d60f41 fix: add VALET_TOKEN_SECRET env var to ci e2e test suite 2023-05-29 13:29:07 +02:00
standardci
fd629d43ba chore(release): publish new version
- @standardnotes/home-server@1.5.1
 - @standardnotes/revisions-server@1.16.1
2023-05-25 11:44:41 +00:00
Karol Sójko
76b1cb0f5a fix(revisions): container bindings 2023-05-25 13:17:53 +02:00
standardci
2f94abc9f7 chore(release): publish new version
- @standardnotes/api-gateway@1.58.0
 - @standardnotes/auth-server@1.111.0
 - @standardnotes/home-server@1.5.0
 - @standardnotes/revisions-server@1.16.0
2023-05-25 11:11:19 +00:00
Karol Sójko
c70040fe5d feat: add revisions service to home server (#613)
* feat: add revisions service to home server

* fix: make e2e test suite on home server non-optional

* fix(auth): specs
2023-05-25 12:57:05 +02:00
130 changed files with 3112 additions and 2364 deletions

View File

@@ -50,7 +50,7 @@ jobs:
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html
e2e-home-server:
name: (WIP - Home Server) E2E Test Suite
name: (Home Server) E2E Test Suite
runs-on: ubuntu-latest
services:
@@ -83,8 +83,10 @@ jobs:
sed -i "s/AUTH_JWT_SECRET=/AUTH_JWT_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
sed -i "s/ENCRYPTION_SERVER_KEY=/ENCRYPTION_SERVER_KEY=$(openssl rand -hex 32)/g" packages/home-server/.env
sed -i "s/PSEUDO_KEY_PARAMS_KEY=/PSEUDO_KEY_PARAMS_KEY=$(openssl rand -hex 32)/g" packages/home-server/.env
sed -i "s/VALET_TOKEN_SECRET=/VALET_TOKEN_SECRET=$(openssl rand -hex 32)/g" packages/home-server/.env
echo "ACCESS_TOKEN_AGE=4" >> packages/home-server/.env
echo "REFRESH_TOKEN_AGE=7" >> packages/home-server/.env
echo "REVISIONS_FREQUENCY=5" >> packages/home-server/.env
- name: Run Server
run: nohup yarn workspace @standardnotes/home-server start &
@@ -92,8 +94,7 @@ jobs:
PORT: 3123
- name: Wait for server to start
run: docker/is-available.sh http://localhost:3123 $(pwd)/logs
run: for i in {1..30}; do curl -s http://localhost:3123/healthcheck && break || sleep 1; done
- name: Run E2E Test Suite
continue-on-error: true
run: yarn dlx mocha-headless-chrome --timeout 1200000 -f http://localhost:9001/mocha/test.html?skip_paid_features=true

12
.pnp.cjs generated
View File

@@ -4626,6 +4626,8 @@ const RAW_RUNTIME_STATE =
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/files-server", "workspace:packages/files"],\
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@types/cors", "npm:2.8.13"],\
["@types/express", "npm:4.17.17"],\
@@ -4733,7 +4735,7 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["winston", "npm:3.8.2"]\
],\
@@ -4925,7 +4927,7 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"],\
["sqlite3", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.1.6"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.0"],\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\
["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"],\
["ua-parser-js", "npm:1.0.35"],\
["uuid", "npm:9.0.0"],\
@@ -15166,10 +15168,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-91f15b21d5/0/cache/typeorm-npm-0.3.15-20a6c4f754-db890f14cb.zip/node_modules/typeorm/",\
["virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15", {\
"packageLocation": "./.yarn/__virtual__/typeorm-virtual-7fe891193c/0/cache/typeorm-npm-0.3.15-20a6c4f754-db890f14cb.zip/node_modules/typeorm/",\
"packageDependencies": [\
["typeorm", "virtual:67ad3a1ca34e24ce4821cc48979e98af0c3e5dd7aabc7ad0b5d22d1d977d6f943f81c9f141a420105ebdc61ef777e508a96c7946081decd98f8c30543d468b33#npm:0.3.15"],\
["typeorm", "virtual:365b8c88cdf194291829ee28b79556e2328175d26a621363e703848100bea0042e9500db2a1206c9bbc3a4a76a1d169639ef774b2ea3a1a98584a9936b58c6be#npm:0.3.15"],\
["@google-cloud/spanner", null],\
["@sap/hana-client", null],\
["@sqltools/formatter", "npm:1.2.5"],\

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.22.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.22.4...@standardnotes/analytics@2.22.5) (2023-05-29)
**Note:** Version bump only for package @standardnotes/analytics
## [2.22.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.22.3...@standardnotes/analytics@2.22.4) (2023-05-17)
**Note:** Version bump only for package @standardnotes/analytics

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.22.4",
"version": "2.22.5",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.59.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.58.0...@standardnotes/api-gateway@1.59.0) (2023-05-29)
### Features
* add files server as a service to home-server ([#614](https://github.com/standardnotes/api-gateway/issues/614)) ([c7d575a](https://github.com/standardnotes/api-gateway/commit/c7d575a0ffc7eb3e8799c3835da5727584f4f67b))
# [1.58.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.57.0...@standardnotes/api-gateway@1.58.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/api-gateway/issues/613)) ([c70040f](https://github.com/standardnotes/api-gateway/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
# [1.57.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.56.2...@standardnotes/api-gateway@1.57.0) (2023-05-25)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.57.0",
"version": "1.59.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ export class RevisionsControllerV2 extends BaseHttpController {
)
}
@httpGet('/:id')
@httpGet('/:uuid')
async getRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(
request,
@@ -37,12 +37,12 @@ export class RevisionsControllerV2 extends BaseHttpController {
'GET',
'items/:itemUuid/revisions/:id',
request.params.itemUuid,
request.params.id,
request.params.uuid,
),
)
}
@httpDelete('/:id')
@httpDelete('/:uuid')
async deleteRevision(request: Request, response: Response): Promise<void> {
await this.httpService.callRevisionsServer(
request,
@@ -51,7 +51,7 @@ export class RevisionsControllerV2 extends BaseHttpController {
'DELETE',
'items/:itemUuid/revisions/:id',
request.params.itemUuid,
request.params.id,
request.params.uuid,
),
)
}

View File

@@ -66,7 +66,12 @@ export class DirectCallServiceProxy implements ServiceProxyInterface {
throw new Error('Revisions service not found')
}
await service.handleRequest(request, response, endpointOrMethodIdentifier)
const serviceResponse = (await service.handleRequest(request, response, endpointOrMethodIdentifier)) as {
statusCode: number
json: Record<string, unknown>
}
this.sendDecoratedResponse(response, serviceResponse)
}
async callSyncingServer(request: never, response: never, endpointOrMethodIdentifier: string): Promise<void> {

View File

@@ -58,6 +58,10 @@ export class EndpointResolver implements EndpointResolverInterface {
['[POST]:items/sync', 'sync.items.sync'],
['[POST]:items/check-integrity', 'sync.items.check_integrity'],
['[GET]:items/:uuid', 'sync.items.get_item'],
// Revisions Controller V2
['[GET]:items/:itemUuid/revisions', 'revisions.revisions.getRevisions'],
['[GET]:items/:itemUuid/revisions/:id', 'revisions.revisions.getRevision'],
['[DELETE]:items/:itemUuid/revisions/:id', 'revisions.revisions.deleteRevision'],
])
resolveEndpointOrMethodIdentifier(method: string, endpoint: string, ...params: string[]): string {

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.112.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.111.0...@standardnotes/auth-server@1.112.0) (2023-05-29)
### Features
* add files server as a service to home-server ([#614](https://github.com/standardnotes/server/issues/614)) ([c7d575a](https://github.com/standardnotes/server/commit/c7d575a0ffc7eb3e8799c3835da5727584f4f67b))
# [1.111.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.110.0...@standardnotes/auth-server@1.111.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/server/issues/613)) ([c70040f](https://github.com/standardnotes/server/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
# [1.110.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.109.2...@standardnotes/auth-server@1.110.0) (2023-05-25)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.110.0",
"version": "1.112.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

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

View File

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

View File

@@ -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')
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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.',
})
}
}

View File

@@ -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)
}
}

View File

@@ -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,
})
}
}

View File

@@ -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,
),
),
)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 })
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import * as express from 'express'
import { InversifyExpressSessionController } from './InversifyExpressSessionController'
import { results } from 'inversify-express-utils'
import { ControllerContainerInterface } from '@standardnotes/domain-core'
import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
@@ -15,20 +14,11 @@ describe('InversifyExpressSessionController', () => {
let refreshSessionToken: RefreshSessionToken
let request: express.Request
let response: express.Response
let controllerContainer: ControllerContainerInterface
const createController = () =>
new InversifyExpressSessionController(
deleteSessionForUser,
deletePreviousSessionsForUser,
refreshSessionToken,
controllerContainer,
)
new InversifyExpressSessionController(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
beforeEach(() => {
controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
controllerContainer.register = jest.fn()
deleteSessionForUser = {} as jest.Mocked<DeleteSessionForUser>
deleteSessionForUser.execute = jest.fn().mockReturnValue({ success: true })
@@ -64,16 +54,12 @@ describe('InversifyExpressSessionController', () => {
},
})
await createController().refresh(request, response)
const httpResult = <results.JsonResult>await createController().refresh(request, response)
const result = await httpResult.executeAsync()
expect(response.send).toHaveBeenCalledWith({
session: {
access_token: '1231',
refresh_token: '2341',
access_expiration: 123123,
refresh_expiration: 123123,
},
})
expect(await result.content.readAsStringAsync()).toEqual(
'{"session":{"access_token":"1231","refresh_token":"2341","access_expiration":123123,"refresh_expiration":123123}}',
)
})
it('should return bad request if tokens are missing from refresh token request', async () => {
@@ -113,14 +99,15 @@ describe('InversifyExpressSessionController', () => {
}
request.body.uuid = '123'
await createController().deleteSession(request, response)
const httpResult = <results.JsonResult>await createController().deleteSession(request, response)
const result = await httpResult.executeAsync()
expect(deleteSessionForUser.execute).toBeCalledWith({
userUuid: '123',
sessionUuid: '123',
})
expect(response.status).toHaveBeenCalledWith(204)
expect(result.statusCode).toEqual(204)
})
it('should not delete a specific session is current session has read only access', async () => {
@@ -205,15 +192,16 @@ describe('InversifyExpressSessionController', () => {
uuid: '234',
},
}
await createController().deleteAllSessions(request, response)
const httpResult = <results.JsonResult>await createController().deleteAllSessions(request, response)
const result = await httpResult.executeAsync()
expect(deletePreviousSessionsForUser.execute).toHaveBeenCalledWith({
userUuid: '123',
currentSessionUuid: '234',
})
expect(response.status).toHaveBeenCalledWith(204)
expect(response.send).toHaveBeenCalled()
expect(result.statusCode).toEqual(204)
})
it('should not delete all sessions if current sessions has read only access', async () => {

View File

@@ -1,8 +1,6 @@
import { ErrorTag } from '@standardnotes/responses'
import { Request, Response } from 'express'
import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpPost,
@@ -13,147 +11,37 @@ import TYPES from '../../Bootstrap/Types'
import { DeletePreviousSessionsForUser } from '../../Domain/UseCase/DeletePreviousSessionsForUser'
import { DeleteSessionForUser } from '../../Domain/UseCase/DeleteSessionForUser'
import { RefreshSessionToken } from '../../Domain/UseCase/RefreshSessionToken'
import { ControllerContainerInterface } from '@standardnotes/domain-core'
import { HomeServerSessionController } from './HomeServer/HomeServerSessionController'
@controller('/session')
export class InversifyExpressSessionController extends BaseHttpController {
export class InversifyExpressSessionController extends HomeServerSessionController {
constructor(
@inject(TYPES.Auth_DeleteSessionForUser) private deleteSessionForUser: DeleteSessionForUser,
@inject(TYPES.Auth_DeleteSessionForUser) override deleteSessionForUser: DeleteSessionForUser,
@inject(TYPES.Auth_DeletePreviousSessionsForUser)
private deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
@inject(TYPES.Auth_RefreshSessionToken) private refreshSessionToken: RefreshSessionToken,
@inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
override deletePreviousSessionsForUser: DeletePreviousSessionsForUser,
@inject(TYPES.Auth_RefreshSessionToken) override refreshSessionToken: RefreshSessionToken,
) {
super()
this.controllerContainer.register('auth.sessions.delete', this.deleteSession.bind(this))
this.controllerContainer.register('auth.sessions.deleteAll', this.deleteAllSessions.bind(this))
this.controllerContainer.register('auth.sessions.refresh', this.refresh.bind(this))
super(deleteSessionForUser, deletePreviousSessionsForUser, refreshSessionToken)
}
@httpDelete('/', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async deleteSession(request: Request, response: Response): Promise<results.JsonResult | void> {
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)
response.status(204).send()
override async deleteSession(
request: Request,
response: Response,
): Promise<results.JsonResult | results.StatusCodeResult> {
return super.deleteSession(request, response)
}
@httpDelete('/all', TYPES.Auth_RequiredCrossServiceTokenMiddleware, TYPES.Auth_SessionMiddleware)
async deleteAllSessions(_request: Request, response: Response): Promise<results.JsonResult | void> {
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)
response.status(204).send()
override async deleteAllSessions(
_request: Request,
response: Response,
): Promise<results.JsonResult | results.StatusCodeResult> {
return super.deleteAllSessions(_request, response)
}
@httpPost('/refresh')
async refresh(request: Request, response: Response): Promise<results.JsonResult | void> {
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)
response.send({
session: result.sessionPayload,
})
override async refresh(request: Request, response: Response): Promise<results.JsonResult> {
return super.refresh(request, response)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import * as express from 'express'
import { InversifyExpressUsersController } from './InversifyExpressUsersController'
import { results } from 'inversify-express-utils'
import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
import { Username } from '@standardnotes/domain-core'
import { DeleteAccount } from '../../Domain/UseCase/DeleteAccount/DeleteAccount'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
@@ -28,7 +28,6 @@ describe('InversifyExpressUsersController', () => {
let request: express.Request
let response: express.Response
let user: User
let controllerContainer: ControllerContainerInterface
const createController = () =>
new InversifyExpressUsersController(
@@ -39,13 +38,9 @@ describe('InversifyExpressUsersController', () => {
clearLoginAttempts,
increaseLoginAttempts,
changeCredentials,
controllerContainer,
)
beforeEach(() => {
controllerContainer = {} as jest.Mocked<ControllerContainerInterface>
controllerContainer.register = jest.fn()
updateUser = {} as jest.Mocked<UpdateUser>
updateUser.execute = jest.fn()
@@ -99,7 +94,8 @@ describe('InversifyExpressUsersController', () => {
updateUser.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } })
await createController().update(request, response)
const httpResponse = <results.JsonResult>await createController().update(request, response)
const result = await httpResponse.executeAsync()
expect(updateUser.execute).toHaveBeenCalledWith({
apiVersion: '20190520',
@@ -112,7 +108,7 @@ describe('InversifyExpressUsersController', () => {
},
})
expect(response.send).toHaveBeenCalledWith({ foo: 'bar' })
expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}')
})
it('should not update user if session has read only access', async () => {
@@ -310,7 +306,8 @@ describe('InversifyExpressUsersController', () => {
changeCredentials.execute = jest.fn().mockReturnValue({ success: true, authResponse: { foo: 'bar' } })
await createController().changeCredentials(request, response)
const httpResponse = <results.JsonResult>await createController().changeCredentials(request, response)
const result = await httpResponse.executeAsync()
expect(changeCredentials.execute).toHaveBeenCalledWith({
apiVersion: '20190520',
@@ -326,7 +323,7 @@ describe('InversifyExpressUsersController', () => {
expect(clearLoginAttempts.execute).toHaveBeenCalled()
expect(response.send).toHaveBeenCalledWith({ foo: 'bar' })
expect(await result.content.readAsStringAsync()).toEqual('{"foo":"bar"}')
})
it('should not change a password if session has read only access', async () => {

View File

@@ -1,8 +1,6 @@
import { Request, Response } from 'express'
import { inject } from 'inversify'
import { ErrorTag } from '@standardnotes/responses'
import {
BaseHttpController,
controller,
httpDelete,
httpGet,
@@ -19,232 +17,52 @@ import { GetUserSubscription } from '../../Domain/UseCase/GetUserSubscription/Ge
import { ClearLoginAttempts } from '../../Domain/UseCase/ClearLoginAttempts'
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
import { ChangeCredentials } from '../../Domain/UseCase/ChangeCredentials/ChangeCredentials'
import { ControllerContainerInterface, Username } from '@standardnotes/domain-core'
import { HomeServerUsersController } from './HomeServer/HomeServerUsersController'
@controller('/users')
export class InversifyExpressUsersController extends BaseHttpController {
export class InversifyExpressUsersController extends HomeServerUsersController {
constructor(
@inject(TYPES.Auth_UpdateUser) private updateUser: UpdateUser,
@inject(TYPES.Auth_GetUserKeyParams) private getUserKeyParams: GetUserKeyParams,
@inject(TYPES.Auth_DeleteAccount) private doDeleteAccount: DeleteAccount,
@inject(TYPES.Auth_GetUserSubscription) private doGetUserSubscription: GetUserSubscription,
@inject(TYPES.Auth_ClearLoginAttempts) private clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) private increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) private changeCredentialsUseCase: ChangeCredentials,
@inject(TYPES.Auth_ControllerContainer) private controllerContainer: ControllerContainerInterface,
@inject(TYPES.Auth_UpdateUser) override updateUser: UpdateUser,
@inject(TYPES.Auth_GetUserKeyParams) override getUserKeyParams: GetUserKeyParams,
@inject(TYPES.Auth_DeleteAccount) override doDeleteAccount: DeleteAccount,
@inject(TYPES.Auth_GetUserSubscription) override doGetUserSubscription: GetUserSubscription,
@inject(TYPES.Auth_ClearLoginAttempts) override clearLoginAttempts: ClearLoginAttempts,
@inject(TYPES.Auth_IncreaseLoginAttempts) override increaseLoginAttempts: IncreaseLoginAttempts,
@inject(TYPES.Auth_ChangeCredentials) override changeCredentialsUseCase: ChangeCredentials,
) {
super()
this.controllerContainer.register('auth.users.update', this.update.bind(this))
this.controllerContainer.register('auth.users.getKeyParams', this.keyParams.bind(this))
this.controllerContainer.register('auth.users.getSubscription', this.getSubscription.bind(this))
this.controllerContainer.register('auth.users.updateCredentials', this.changeCredentials.bind(this))
}
@httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async update(request: Request, response: Response): Promise<results.JsonResult | void> {
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)
response.send(updateResult.authResponse)
return
}
return this.json(
{
error: {
message: 'Could not update user.',
},
},
400,
super(
updateUser,
getUserKeyParams,
doDeleteAccount,
doGetUserSubscription,
clearLoginAttempts,
increaseLoginAttempts,
changeCredentialsUseCase,
)
}
@httpPatch('/:userId', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
override async update(request: Request, response: Response): Promise<results.JsonResult> {
return super.update(request, response)
}
@httpGet('/params')
async keyParams(request: Request): Promise<results.JsonResult> {
const email = 'email' in request.query ? <string>request.query.email : undefined
const userUuid = 'uuid' in request.query ? <string>request.query.uuid : undefined
if (!email && !userUuid) {
return this.json(
{
error: {
message: 'Missing mandatory request query parameters.',
},
},
400,
)
}
const result = await this.getUserKeyParams.execute({
email,
userUuid,
authenticated: request.query.authenticated === 'true',
})
return this.json(result.keyParams)
override async keyParams(request: Request): Promise<results.JsonResult> {
return super.keyParams(request)
}
@httpDelete('/:email')
async deleteAccount(request: Request): Promise<results.JsonResult> {
const result = await this.doDeleteAccount.execute({
email: request.params.email,
})
return this.json({ message: result.message }, result.responseCode)
override async deleteAccount(request: Request): Promise<results.JsonResult> {
return super.deleteAccount(request)
}
@httpGet('/:userUuid/subscription', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
if (request.params.userUuid !== response.locals.user.uuid) {
return this.json(
{
error: {
message: 'Operation not allowed.',
},
},
401,
)
}
const result = await this.doGetUserSubscription.execute({
userUuid: request.params.userUuid,
})
if (result.success) {
return this.json(result)
}
return this.json(result, 400)
override async getSubscription(request: Request, response: Response): Promise<results.JsonResult> {
return super.getSubscription(request, response)
}
@httpPut('/:userId/attributes/credentials', TYPES.Auth_RequiredCrossServiceTokenMiddleware)
async changeCredentials(request: Request, response: Response): Promise<results.JsonResult | void> {
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)
response.send(changeCredentialsResult.authResponse)
override async changeCredentials(request: Request, response: Response): Promise<results.JsonResult> {
return super.changeCredentials(request, response)
}
}

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.15.0...@standardnotes/domain-core@1.16.0) (2023-05-29)
### Features
* add files server as a service to home-server ([#614](https://github.com/standardnotes/server/issues/614)) ([c7d575a](https://github.com/standardnotes/server/commit/c7d575a0ffc7eb3e8799c3835da5727584f4f67b))
# [1.15.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.14.2...@standardnotes/domain-core@1.15.0) (2023-05-16)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.15.0",
"version": "1.16.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

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

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.13.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.12.5...@standardnotes/files-server@1.13.0) (2023-05-29)
### Features
* add files server as a service to home-server ([#614](https://github.com/standardnotes/files/issues/614)) ([c7d575a](https://github.com/standardnotes/files/commit/c7d575a0ffc7eb3e8799c3835da5727584f4f67b))
## [1.12.5](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.12.4...@standardnotes/files-server@1.12.5) (2023-05-18)
**Note:** Version bump only for package @standardnotes/files-server

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.12.5",
"version": "1.13.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -18,6 +18,7 @@
"setup:env": "cp .env.sample .env",
"build": "tsc --build",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --fix --ext .ts",
"pretest": "yarn lint && yarn build",
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
"start": "yarn node dist/bin/server.js",

View File

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

View File

@@ -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()
}
}

View File

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

View File

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

View File

@@ -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')
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.6.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.5.1...@standardnotes/home-server@1.6.0) (2023-05-29)
### Features
* add files server as a service to home-server ([#614](https://github.com/standardnotes/server/issues/614)) ([c7d575a](https://github.com/standardnotes/server/commit/c7d575a0ffc7eb3e8799c3835da5727584f4f67b))
## [1.5.1](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.5.0...@standardnotes/home-server@1.5.1) (2023-05-25)
**Note:** Version bump only for package @standardnotes/home-server
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.4.4...@standardnotes/home-server@1.5.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/server/issues/613)) ([c70040f](https://github.com/standardnotes/server/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
## [1.4.4](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.4.3...@standardnotes/home-server@1.4.4) (2023-05-25)
**Note:** Version bump only for package @standardnotes/home-server

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.4.4",
"version": "1.6.0",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -22,6 +22,8 @@
"@standardnotes/auth-server": "workspace:^",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events-infra": "workspace:^",
"@standardnotes/files-server": "workspace:^",
"@standardnotes/revisions-server": "workspace:^",
"@standardnotes/syncing-server": "workspace:^",
"cors": "2.8.5",
"dotenv": "^16.0.1",

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.17.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.16.1...@standardnotes/revisions-server@1.17.0) (2023-05-29)
### Features
* add files server as a service to home-server ([#614](https://github.com/standardnotes/server/issues/614)) ([c7d575a](https://github.com/standardnotes/server/commit/c7d575a0ffc7eb3e8799c3835da5727584f4f67b))
## [1.16.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.16.0...@standardnotes/revisions-server@1.16.1) (2023-05-25)
### Bug Fixes
* **revisions:** container bindings ([76b1cb0](https://github.com/standardnotes/server/commit/76b1cb0f5a01c547f7bd9ba3475c3988dcb9afdb))
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.15.1...@standardnotes/revisions-server@1.16.0) (2023-05-25)
### Features
* add revisions service to home server ([#613](https://github.com/standardnotes/server/issues/613)) ([c70040f](https://github.com/standardnotes/server/commit/c70040fe5dfd35663b9811fbbaa9370bd0298482))
## [1.15.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.15.0...@standardnotes/revisions-server@1.15.1) (2023-05-18)
**Note:** Version bump only for package @standardnotes/revisions-server

Some files were not shown because too many files have changed in this diff Show More