mirror of
https://github.com/standardnotes/server
synced 2026-04-01 03:01:12 -04:00
Compare commits
38 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55587f6207 | ||
|
|
0d6b45c795 | ||
|
|
95f64d9952 | ||
|
|
54da5def4b | ||
|
|
d2fc1e057d | ||
|
|
0a90d98c71 | ||
|
|
cc269e3b35 | ||
|
|
b19093179b | ||
|
|
e2cc0bc003 | ||
|
|
644c52ae36 | ||
|
|
2554273a3f | ||
|
|
a8ee149d7a | ||
|
|
dcf92d58f9 | ||
|
|
053092031c | ||
|
|
c12e3eb3ec | ||
|
|
07def20f6b | ||
|
|
6c2cca66bd | ||
|
|
6efd336f34 | ||
|
|
81eb4be200 | ||
|
|
76cee6dbad | ||
|
|
dcc35a5738 | ||
|
|
5628de6445 | ||
|
|
53bea47727 | ||
|
|
d6cf8d400a | ||
|
|
b58cc335f2 | ||
|
|
03d1bc611c | ||
|
|
a48b09cefe | ||
|
|
d3f36c05df | ||
|
|
488ade25ab | ||
|
|
413a276d20 | ||
|
|
65675a21d6 | ||
|
|
d35de38289 | ||
|
|
83e1baa978 | ||
|
|
875edce5b1 | ||
|
|
1baa504728 | ||
|
|
965ae79414 | ||
|
|
7a8448c116 | ||
|
|
d935157ee8 |
15
.pnp.cjs
generated
15
.pnp.cjs
generated
@@ -2567,7 +2567,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
["@standardnotes/features", "npm:1.50.0"],\
|
||||
["@standardnotes/features", "npm:1.52.1"],\
|
||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||
["@standardnotes/responses", "npm:1.6.39"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
@@ -2651,7 +2651,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.50.0"],\
|
||||
["@standardnotes/features", "npm:1.52.1"],\
|
||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@types/jest", "npm:28.1.4"],\
|
||||
@@ -2753,6 +2753,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.52.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.52.1-1fee85cf4e-ff3684399e.zip/node_modules/@standardnotes/features/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/features", "npm:1.52.1"],\
|
||||
["@standardnotes/auth", "npm:3.19.4"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/files-server", [\
|
||||
|
||||
BIN
.yarn/cache/@standardnotes-features-npm-1.52.1-1fee85cf4e-ff3684399e.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-features-npm-1.52.1-1fee85cf4e-ff3684399e.zip
vendored
Normal file
Binary file not shown.
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.22.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.1...@standardnotes/api-gateway@1.22.2) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.22.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.22.0...@standardnotes/api-gateway@1.22.1) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** remove admin graphql endpoint from being publicly available ([0a90d98](https://github.com/standardnotes/api-gateway/commit/0a90d98c71c6023b700f852c91aedfe1ad23af55))
|
||||
|
||||
# [1.22.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.21.1...@standardnotes/api-gateway@1.22.0) (2022-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** remove muting emails by use case in favor of updating user settings ([d3f36c0](https://github.com/standardnotes/api-gateway/commit/d3f36c05dfc114098a6c231d81149ebd1a959b74))
|
||||
|
||||
## [1.21.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.21.0...@standardnotes/api-gateway@1.21.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-gateway:** web socket connection routing ([d35de38](https://github.com/standardnotes/api-gateway/commit/d35de38289e70d707d57a859b8bf39833fa825dd))
|
||||
|
||||
# [1.21.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.20.0...@standardnotes/api-gateway@1.21.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating cross service token in exchange for web socket connection token ([965ae79](https://github.com/standardnotes/api-gateway/commit/965ae79414e25d0959f67e16dcbb054229013e1c))
|
||||
|
||||
# [1.20.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.6...@standardnotes/api-gateway@1.20.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionToken
|
||||
import { StatisticsMiddleware } from '../Controller/StatisticsMiddleware'
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
|
||||
import { WebSocketAuthMiddleware } from '../Controller/WebSocketAuthMiddleware'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -85,6 +86,7 @@ export class ContainerConfigLoader {
|
||||
|
||||
// Middleware
|
||||
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
|
||||
container.bind<WebSocketAuthMiddleware>(TYPES.WebSocketAuthMiddleware).to(WebSocketAuthMiddleware)
|
||||
container
|
||||
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)
|
||||
.to(SubscriptionTokenAuthMiddleware)
|
||||
|
||||
@@ -18,6 +18,7 @@ const TYPES = {
|
||||
// Middleware
|
||||
StatisticsMiddleware: Symbol.for('StatisticsMiddleware'),
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
WebSocketAuthMiddleware: Symbol.for('WebSocketAuthMiddleware'),
|
||||
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
|
||||
// Services
|
||||
HTTPService: Symbol.for('HTTPService'),
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { verify } from 'jsonwebtoken'
|
||||
import { AxiosError, AxiosInstance } from 'axios'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class WebSocketAuthMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
|
||||
@inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
|
||||
const authHeaderValue = request.headers.authorization as string
|
||||
|
||||
if (!authHeaderValue) {
|
||||
response.status(401).send({
|
||||
error: {
|
||||
tag: 'invalid-auth',
|
||||
message: 'Invalid login credentials.',
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const authResponse = await this.httpClient.request({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authHeaderValue,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
validateStatus: (status: number) => {
|
||||
return status >= 200 && status < 500
|
||||
},
|
||||
url: `${this.authServerUrl}/sockets/tokens/validate`,
|
||||
})
|
||||
|
||||
if (authResponse.status > 200) {
|
||||
response.setHeader('content-type', authResponse.headers['content-type'])
|
||||
response.status(authResponse.status).send(authResponse.data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const crossServiceToken = authResponse.data.authToken
|
||||
|
||||
response.locals.authToken = crossServiceToken
|
||||
|
||||
const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
|
||||
|
||||
response.locals.freeUser =
|
||||
decodedToken.roles.length === 1 &&
|
||||
decodedToken.roles.find((role) => role.name === RoleName.CoreUser) !== undefined
|
||||
response.locals.userUuid = decodedToken.user.uuid
|
||||
response.locals.roles = decodedToken.roles
|
||||
} catch (error) {
|
||||
const errorMessage = (error as AxiosError).isAxiosError
|
||||
? JSON.stringify((error as AxiosError).response?.data)
|
||||
: (error as Error).message
|
||||
|
||||
this.logger.error(
|
||||
`Could not pass the request to ${this.authServerUrl}/sockets/tokens/validate on underlying service: ${errorMessage}`,
|
||||
)
|
||||
|
||||
this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
|
||||
|
||||
if ((error as AxiosError).response?.headers['content-type']) {
|
||||
response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
|
||||
}
|
||||
|
||||
const errorCode =
|
||||
(error as AxiosError).isAxiosError && !isNaN(+((error as AxiosError).code as string))
|
||||
? +((error as AxiosError).code as string)
|
||||
: 500
|
||||
|
||||
response.status(errorCode).send(errorMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -29,34 +29,4 @@ export class ActionsController extends BaseHttpController {
|
||||
async methods(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
|
||||
}
|
||||
|
||||
@httpGet('/failed-backups-emails/mute/:settingUuid')
|
||||
async muteFailedBackupsEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/email_backup/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/sign-in-emails/mute/:settingUuid')
|
||||
async muteSignInEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/sign_in/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpGet('/marketing-emails/mute/:settingUuid')
|
||||
async muteMarketingEmails(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`internal/settings/marketing-emails/${request.params.settingUuid}/mute`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +70,6 @@ export class PaymentsController extends BaseHttpController {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/events/registration', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/graphql')
|
||||
async adminGraphql(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/graphql', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/admin/auth/login')
|
||||
async adminLogin(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callPaymentsServer(request, response, 'admin/auth/login', request.body)
|
||||
|
||||
@@ -20,7 +20,7 @@ export class WebSocketsController extends BaseHttpController {
|
||||
await this.httpService.callAuthServer(request, response, 'sockets/tokens', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
@httpPost('/connections', TYPES.WebSocketAuthMiddleware)
|
||||
async createWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
this.logger.error('Could not create a websocket connection. Missing connection id header.')
|
||||
@@ -30,10 +30,15 @@ export class WebSocketsController extends BaseHttpController {
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`sockets/connections/${request.headers.connectionid}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
|
||||
@httpDelete('/')
|
||||
@httpDelete('/connections')
|
||||
async deleteWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
this.logger.error('Could not delete a websocket connection. Missing connection id header.')
|
||||
@@ -43,6 +48,11 @@ export class WebSocketsController extends BaseHttpController {
|
||||
return
|
||||
}
|
||||
|
||||
await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
|
||||
await this.httpService.callAuthServer(
|
||||
request,
|
||||
response,
|
||||
`sockets/connections/${request.headers.connectionid}`,
|
||||
request.body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,88 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.32.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.8...@standardnotes/auth-server@1.32.9) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.32.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.7...@standardnotes/auth-server@1.32.8) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** ttl for lock counter on login lockout ([54da5de](https://github.com/standardnotes/server/commit/54da5def4bbfbb4f74cbf02ae23e45103d250dd9))
|
||||
|
||||
## [1.32.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.6...@standardnotes/auth-server@1.32.7) (2022-09-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** allow resending canceled subscription invites ([b190931](https://github.com/standardnotes/server/commit/b19093179baaa1fb8cdf3f9d9bee20e625ed0b9b))
|
||||
|
||||
## [1.32.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.5...@standardnotes/auth-server@1.32.6) (2022-09-22)
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "fix(auth): subscription token ttl" ([644c52a](https://github.com/standardnotes/server/commit/644c52ae36d3720dee0712e2cb826c7e617ab7b7))
|
||||
* Revert "fix(auth): increase subscription token ttl" ([2554273](https://github.com/standardnotes/server/commit/2554273a3f85a968fed4286d109bed5413ef9908))
|
||||
* Revert "tmp(auth): disable expiring of subscription tokens" ([a8ee149](https://github.com/standardnotes/server/commit/a8ee149d7ac78775bf447ab924458b116414a15e))
|
||||
|
||||
## [1.32.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.4...@standardnotes/auth-server@1.32.5) (2022-09-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.32.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.3...@standardnotes/auth-server@1.32.4) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** increase subscription token ttl ([07def20](https://github.com/standardnotes/server/commit/07def20f6b47f9d1c678cfe5206b924dd5e6014a))
|
||||
|
||||
## [1.32.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.2...@standardnotes/auth-server@1.32.3) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** subscription token ttl ([6efd336](https://github.com/standardnotes/server/commit/6efd336f3407e7204a0c5d385ea9df5c02c7e5f5))
|
||||
|
||||
## [1.32.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.1...@standardnotes/auth-server@1.32.2) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add throwing an error if the subscription token was not persisted ([76cee6d](https://github.com/standardnotes/server/commit/76cee6dbad9bff041d8d5a1d4435046509c14f71))
|
||||
|
||||
## [1.32.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.32.0...@standardnotes/auth-server@1.32.1) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** settings and subscription settings projection ([d6cf8d4](https://github.com/standardnotes/server/commit/d6cf8d400a0177ee9030a171cf2ca47ade293fd9))
|
||||
|
||||
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.2...@standardnotes/auth-server@1.32.0) (2022-09-22)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** remove muting emails by use case in favor of updating user settings ([d3f36c0](https://github.com/standardnotes/server/commit/d3f36c05dfc114098a6c231d81149ebd1a959b74))
|
||||
|
||||
## [1.31.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.1...@standardnotes/auth-server@1.31.2) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** response wrapping on web socket connection token creation ([413a276](https://github.com/standardnotes/server/commit/413a276d205d53c316f7d0af8aed422001a6c1ab))
|
||||
|
||||
## [1.31.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.31.0...@standardnotes/auth-server@1.31.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** web sockets routes ([875edce](https://github.com/standardnotes/server/commit/875edce5b1dc134b4e22702354b29303fab3c910))
|
||||
|
||||
# [1.31.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.30.1...@standardnotes/auth-server@1.31.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating cross service token in exchange for web socket connection token ([965ae79](https://github.com/standardnotes/server/commit/965ae79414e25d0959f67e16dcbb054229013e1c))
|
||||
|
||||
## [1.30.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.30.0...@standardnotes/auth-server@1.30.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** missing injectable annotation ([d935157](https://github.com/standardnotes/server/commit/d935157ee8425d427fa52465e766d18e29332b5b))
|
||||
|
||||
# [1.30.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.29.1...@standardnotes/auth-server@1.30.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.30.0",
|
||||
"version": "1.32.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -38,7 +38,7 @@
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/features": "^1.47.0",
|
||||
"@standardnotes/features": "^1.52.1",
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
"@standardnotes/responses": "^1.6.39",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
|
||||
@@ -143,7 +143,6 @@ import { ApiGatewayOfflineAuthMiddleware } from '../Controller/ApiGatewayOffline
|
||||
import { UserEmailChangedEventHandler } from '../Domain/Handler/UserEmailChangedEventHandler'
|
||||
import { SettingsAssociationServiceInterface } from '../Domain/Setting/SettingsAssociationServiceInterface'
|
||||
import { SettingsAssociationService } from '../Domain/Setting/SettingsAssociationService'
|
||||
import { MuteFailedBackupsEmails } from '../Domain/UseCase/MuteFailedBackupsEmails/MuteFailedBackupsEmails'
|
||||
import { SubscriptionSyncRequestedEventHandler } from '../Domain/Handler/SubscriptionSyncRequestedEventHandler'
|
||||
import {
|
||||
CrossServiceTokenData,
|
||||
@@ -163,7 +162,6 @@ import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValet
|
||||
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
||||
import { ListedAccountCreatedEventHandler } from '../Domain/Handler/ListedAccountCreatedEventHandler'
|
||||
import { ListedAccountDeletedEventHandler } from '../Domain/Handler/ListedAccountDeletedEventHandler'
|
||||
import { MuteSignInEmails } from '../Domain/UseCase/MuteSignInEmails/MuteSignInEmails'
|
||||
import { FileRemovedEventHandler } from '../Domain/Handler/FileRemovedEventHandler'
|
||||
import { UserDisabledSessionUserAgentLoggingEventHandler } from '../Domain/Handler/UserDisabledSessionUserAgentLoggingEventHandler'
|
||||
import { SettingInterpreterInterface } from '../Domain/Setting/SettingInterpreterInterface'
|
||||
@@ -204,7 +202,6 @@ import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUser
|
||||
import { AuthController } from '../Controller/AuthController'
|
||||
import { VerifyPredicate } from '../Domain/UseCase/VerifyPredicate/VerifyPredicate'
|
||||
import { PredicateVerificationRequestedEventHandler } from '../Domain/Handler/PredicateVerificationRequestedEventHandler'
|
||||
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
|
||||
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
|
||||
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
@@ -212,6 +209,7 @@ import { SubscriptionInvitesController } from '../Controller/SubscriptionInvites
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
import { WebSocketsController } from '../Controller/WebSocketsController'
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -438,9 +436,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<CreateOfflineSubscriptionToken>(TYPES.CreateOfflineSubscriptionToken)
|
||||
.to(CreateOfflineSubscriptionToken)
|
||||
container.bind<MuteFailedBackupsEmails>(TYPES.MuteFailedBackupsEmails).to(MuteFailedBackupsEmails)
|
||||
container.bind<MuteSignInEmails>(TYPES.MuteSignInEmails).to(MuteSignInEmails)
|
||||
container.bind<MuteMarketingEmails>(TYPES.MuteMarketingEmails).to(MuteMarketingEmails)
|
||||
container.bind<CreateValetToken>(TYPES.CreateValetToken).to(CreateValetToken)
|
||||
container.bind<CreateListedAccount>(TYPES.CreateListedAccount).to(CreateListedAccount)
|
||||
container.bind<InviteToSharedSubscription>(TYPES.InviteToSharedSubscription).to(InviteToSharedSubscription)
|
||||
@@ -462,6 +457,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<CreateWebSocketConnectionToken>(TYPES.CreateWebSocketConnectionToken)
|
||||
.to(CreateWebSocketConnectionToken)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
@@ -534,6 +530,11 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<TokenDecoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenDecoder)
|
||||
.toConstantValue(new TokenDecoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
|
||||
container
|
||||
.bind<TokenDecoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenDecoder)
|
||||
.toConstantValue(
|
||||
new TokenDecoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
|
||||
)
|
||||
container
|
||||
.bind<TokenEncoderInterface<OfflineUserTokenData>>(TYPES.OfflineUserTokenEncoder)
|
||||
.toConstantValue(new TokenEncoder<OfflineUserTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))
|
||||
|
||||
@@ -115,9 +115,6 @@ const TYPES = {
|
||||
AuthenticateSubscriptionToken: Symbol.for('AuthenticateSubscriptionToken'),
|
||||
CreateOfflineSubscriptionToken: Symbol.for('CreateOfflineSubscriptionToken'),
|
||||
AuthenticateOfflineSubscriptionToken: Symbol.for('AuthenticateOfflineSubscriptionToken'),
|
||||
MuteFailedBackupsEmails: Symbol.for('MuteFailedBackupsEmails'),
|
||||
MuteSignInEmails: Symbol.for('MuteSignInEmails'),
|
||||
MuteMarketingEmails: Symbol.for('MuteMarketingEmails'),
|
||||
CreateValetToken: Symbol.for('CreateValetToken'),
|
||||
CreateListedAccount: Symbol.for('CreateListedAccount'),
|
||||
InviteToSharedSubscription: Symbol.for('InviteToSharedSubscription'),
|
||||
@@ -129,6 +126,7 @@ const TYPES = {
|
||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||
VerifyPredicate: Symbol.for('VerifyPredicate'),
|
||||
CreateWebSocketConnectionToken: Symbol.for('CreateWebSocketConnectionToken'),
|
||||
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
@@ -171,6 +169,7 @@ const TYPES = {
|
||||
SessionTokenEncoder: Symbol.for('SessionTokenEncoder'),
|
||||
ValetTokenEncoder: Symbol.for('ValetTokenEncoder'),
|
||||
WebSocketConnectionTokenEncoder: Symbol.for('WebSocketConnectionTokenEncoder'),
|
||||
WebSocketConnectionTokenDecoder: Symbol.for('WebSocketConnectionTokenDecoder'),
|
||||
AuthenticationMethodResolver: Symbol.for('AuthenticationMethodResolver'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
|
||||
@@ -7,23 +7,16 @@ import { results } from 'inversify-express-utils'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { GetUserFeatures } from '../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { MuteFailedBackupsEmails } from '../Domain/UseCase/MuteFailedBackupsEmails/MuteFailedBackupsEmails'
|
||||
import { MuteSignInEmails } from '../Domain/UseCase/MuteSignInEmails/MuteSignInEmails'
|
||||
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
|
||||
|
||||
describe('InternalController', () => {
|
||||
let getUserFeatures: GetUserFeatures
|
||||
let getSetting: GetSetting
|
||||
let muteFailedBackupsEmails: MuteFailedBackupsEmails
|
||||
let muteSignInEmails: MuteSignInEmails
|
||||
let muteMarketingEmails: MuteMarketingEmails
|
||||
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let user: User
|
||||
|
||||
const createController = () =>
|
||||
new InternalController(getUserFeatures, getSetting, muteFailedBackupsEmails, muteSignInEmails, muteMarketingEmails)
|
||||
const createController = () => new InternalController(getUserFeatures, getSetting)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
@@ -35,15 +28,6 @@ describe('InternalController', () => {
|
||||
getSetting = {} as jest.Mocked<GetSetting>
|
||||
getSetting.execute = jest.fn()
|
||||
|
||||
muteFailedBackupsEmails = {} as jest.Mocked<MuteFailedBackupsEmails>
|
||||
muteFailedBackupsEmails.execute = jest.fn()
|
||||
|
||||
muteSignInEmails = {} as jest.Mocked<MuteSignInEmails>
|
||||
muteSignInEmails.execute = jest.fn()
|
||||
|
||||
muteMarketingEmails = {} as jest.Mocked<MuteMarketingEmails>
|
||||
muteMarketingEmails.execute = jest.fn()
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
body: {},
|
||||
@@ -120,83 +104,4 @@ describe('InternalController', () => {
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should mute failed backup emails user setting', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteFailedBackupsEmails.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteFailedBackupsEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteFailedBackupsEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not mute failed backup emails user setting if it does not exist', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteFailedBackupsEmails.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteFailedBackupsEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteFailedBackupsEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(404)
|
||||
})
|
||||
|
||||
it('should mute sign in emails user setting', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteSignInEmails.execute = jest.fn().mockReturnValue({ success: true })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteSignInEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteSignInEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(200)
|
||||
})
|
||||
|
||||
it('should not mute sign in emails user setting if it does not exist', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteSignInEmails.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = <results.JsonResult>await createController().muteSignInEmails(request)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(muteSignInEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(result.statusCode).toEqual(404)
|
||||
})
|
||||
|
||||
it('should mute marketing emails user setting', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteMarketingEmails.execute = jest.fn().mockReturnValue({ success: true, message: 'foobar' })
|
||||
|
||||
await createController().muteMarketingEmails(request, response)
|
||||
|
||||
expect(muteMarketingEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledWith('content-type', 'text/html')
|
||||
expect(response.send).toHaveBeenCalledWith('foobar')
|
||||
})
|
||||
|
||||
it('should not mute marketing emails user setting if it does not exist', async () => {
|
||||
request.params.settingUuid = '1-2-3'
|
||||
|
||||
muteMarketingEmails.execute = jest.fn().mockReturnValue({ success: false, message: 'foobar' })
|
||||
|
||||
await createController().muteMarketingEmails(request, response)
|
||||
|
||||
expect(muteMarketingEmails.execute).toHaveBeenCalledWith({ settingUuid: '1-2-3' })
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledWith('content-type', 'text/html')
|
||||
expect(response.status).toHaveBeenCalledWith(404)
|
||||
expect(response.send).toHaveBeenCalledWith('foobar')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { Request } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
@@ -10,18 +10,12 @@ import {
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { GetSetting } from '../Domain/UseCase/GetSetting/GetSetting'
|
||||
import { GetUserFeatures } from '../Domain/UseCase/GetUserFeatures/GetUserFeatures'
|
||||
import { MuteFailedBackupsEmails } from '../Domain/UseCase/MuteFailedBackupsEmails/MuteFailedBackupsEmails'
|
||||
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
|
||||
import { MuteSignInEmails } from '../Domain/UseCase/MuteSignInEmails/MuteSignInEmails'
|
||||
|
||||
@controller('/internal')
|
||||
export class InternalController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserFeatures) private doGetUserFeatures: GetUserFeatures,
|
||||
@inject(TYPES.GetSetting) private doGetSetting: GetSetting,
|
||||
@inject(TYPES.MuteFailedBackupsEmails) private doMuteFailedBackupsEmails: MuteFailedBackupsEmails,
|
||||
@inject(TYPES.MuteSignInEmails) private doMuteSignInEmails: MuteSignInEmails,
|
||||
@inject(TYPES.MuteMarketingEmails) private doMuteMarketingEmails: MuteMarketingEmails,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -54,50 +48,4 @@ export class InternalController extends BaseHttpController {
|
||||
|
||||
return this.json(result, 400)
|
||||
}
|
||||
|
||||
@httpGet('/settings/email_backup/:settingUuid/mute')
|
||||
async muteFailedBackupsEmails(request: Request): Promise<results.JsonResult> {
|
||||
const { settingUuid } = request.params
|
||||
const result = await this.doMuteFailedBackupsEmails.execute({
|
||||
settingUuid,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json({ message: result.message })
|
||||
}
|
||||
|
||||
return this.json({ message: result.message }, 404)
|
||||
}
|
||||
|
||||
@httpGet('/settings/sign_in/:settingUuid/mute')
|
||||
async muteSignInEmails(request: Request): Promise<results.JsonResult> {
|
||||
const { settingUuid } = request.params
|
||||
const result = await this.doMuteSignInEmails.execute({
|
||||
settingUuid,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
return this.json({ message: result.message })
|
||||
}
|
||||
|
||||
return this.json({ message: result.message }, 404)
|
||||
}
|
||||
|
||||
@httpGet('/settings/marketing-emails/:settingUuid/mute')
|
||||
async muteMarketingEmails(request: Request, response: Response): Promise<void> {
|
||||
const { settingUuid } = request.params
|
||||
const result = await this.doMuteMarketingEmails.execute({
|
||||
settingUuid,
|
||||
})
|
||||
|
||||
response.setHeader('content-type', 'text/html')
|
||||
|
||||
if (result.success) {
|
||||
response.send(result.message)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
response.status(404).send(result.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,43 +9,25 @@ import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { AuthenticateRequest } from '../Domain/UseCase/AuthenticateRequest'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
|
||||
describe('SessionsController', () => {
|
||||
let getActiveSessionsForUser: GetActiveSessionsForUser
|
||||
let authenticateRequest: AuthenticateRequest
|
||||
let userProjector: ProjectorInterface<User>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
const jwtTTL = 60
|
||||
let sessionProjector: ProjectorInterface<Session>
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let session: Session
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let user: User
|
||||
let role: Role
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let createCrossServiceToken: CreateCrossServiceToken
|
||||
|
||||
const createController = () =>
|
||||
new SessionsController(
|
||||
getActiveSessionsForUser,
|
||||
authenticateRequest,
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
true,
|
||||
jwtTTL,
|
||||
)
|
||||
new SessionsController(getActiveSessionsForUser, authenticateRequest, sessionProjector, createCrossServiceToken)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.roles = Promise.resolve([role])
|
||||
|
||||
getActiveSessionsForUser = {} as jest.Mocked<GetActiveSessionsForUser>
|
||||
getActiveSessionsForUser.execute = jest.fn().mockReturnValue({ sessions: [session] })
|
||||
@@ -53,21 +35,11 @@ describe('SessionsController', () => {
|
||||
authenticateRequest = {} as jest.Mocked<AuthenticateRequest>
|
||||
authenticateRequest.execute = jest.fn()
|
||||
|
||||
userProjector = {} as jest.Mocked<ProjectorInterface<User>>
|
||||
userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' })
|
||||
|
||||
roleProjector = {} as jest.Mocked<ProjectorInterface<Role>>
|
||||
roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' })
|
||||
|
||||
sessionProjector = {} as jest.Mocked<ProjectorInterface<Session>>
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
sessionProjector.projectSimple = jest.fn().mockReturnValue({ test: 'test' })
|
||||
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<CrossServiceTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 123 })
|
||||
createCrossServiceToken = {} as jest.Mocked<CreateCrossServiceToken>
|
||||
createCrossServiceToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
|
||||
request = {
|
||||
params: {},
|
||||
@@ -114,75 +86,6 @@ describe('SessionsController', () => {
|
||||
const httpResponseContent = await result.content.readAsStringAsync()
|
||||
const httpResponseJSON = JSON.parse(httpResponseContent)
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
|
||||
expect(httpResponseJSON.authToken).toEqual('foobar')
|
||||
})
|
||||
|
||||
it('should validate a session from an incoming request - disabled analytics', async () => {
|
||||
authenticateRequest.execute = jest.fn().mockReturnValue({
|
||||
success: true,
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
request.headers.authorization = 'test'
|
||||
|
||||
const controller = new SessionsController(
|
||||
getActiveSessionsForUser,
|
||||
authenticateRequest,
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
false,
|
||||
jwtTTL,
|
||||
)
|
||||
|
||||
const httpResponse = await controller.validate(request)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.JsonResult)
|
||||
|
||||
const result = await httpResponse.executeAsync()
|
||||
const httpResponseContent = await result.content.readAsStringAsync()
|
||||
const httpResponseJSON = JSON.parse(httpResponseContent)
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
|
||||
expect(httpResponseJSON.authToken).toEqual('foobar')
|
||||
})
|
||||
|
||||
|
||||
@@ -12,26 +12,18 @@ import TYPES from '../Bootstrap/Types'
|
||||
import { Session } from '../Domain/Session/Session'
|
||||
import { AuthenticateRequest } from '../Domain/UseCase/AuthenticateRequest'
|
||||
import { GetActiveSessionsForUser } from '../Domain/UseCase/GetActiveSessionsForUser'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
import { SessionProjector } from '../Projection/SessionProjector'
|
||||
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
|
||||
@controller('/sessions')
|
||||
export class SessionsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.GetActiveSessionsForUser) private getActiveSessionsForUser: GetActiveSessionsForUser,
|
||||
@inject(TYPES.AuthenticateRequest) private authenticateRequest: AuthenticateRequest,
|
||||
@inject(TYPES.UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@inject(TYPES.RoleProjector) private roleProjector: ProjectorInterface<Role>,
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.ANALYTICS_ENABLED) private analyticsEnabled: boolean,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
@inject(TYPES.CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -56,25 +48,12 @@ export class SessionsController extends BaseHttpController {
|
||||
|
||||
const user = authenticateRequestResponse.user as User
|
||||
|
||||
const roles = await user.roles
|
||||
const result = await this.createCrossServiceToken.execute({
|
||||
user,
|
||||
session: authenticateRequestResponse.session,
|
||||
})
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
}
|
||||
|
||||
if (this.analyticsEnabled) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
authTokenData.analyticsId = analyticsId
|
||||
}
|
||||
|
||||
if (authenticateRequestResponse.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(authenticateRequestResponse.session)
|
||||
}
|
||||
|
||||
const authToken = this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL)
|
||||
|
||||
return this.json({ authToken })
|
||||
return this.json({ authToken: result.token })
|
||||
}
|
||||
|
||||
@httpGet('/', TYPES.AuthMiddleware, TYPES.SessionMiddleware)
|
||||
@@ -93,36 +72,4 @@ export class SessionsController extends BaseHttpController {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
return <{ uuid: string; email: string }>this.userProjector.projectSimple(user)
|
||||
}
|
||||
|
||||
private projectSession(session: Session): {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
} {
|
||||
return <
|
||||
{
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
>this.sessionProjector.projectSimple(session)
|
||||
}
|
||||
|
||||
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
|
||||
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ import { Uuid } from '@standardnotes/common'
|
||||
import { SubscriptionToken } from './SubscriptionToken'
|
||||
|
||||
export interface SubscriptionTokenRepositoryInterface {
|
||||
save(subscriptionToken: SubscriptionToken): Promise<void>
|
||||
save(subscriptionToken: SubscriptionToken): Promise<boolean>
|
||||
getUserUuidByToken(token: string): Promise<Uuid | undefined>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
|
||||
describe('CreateCrossServiceToken', () => {
|
||||
let userProjector: ProjectorInterface<User>
|
||||
let sessionProjector: ProjectorInterface<Session>
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let userRepository: UserRepositoryInterface
|
||||
const jwtTTL = 60
|
||||
|
||||
let session: Session
|
||||
let user: User
|
||||
let role: Role
|
||||
|
||||
const createUseCase = (analyticsEnabled = true) =>
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
userRepository,
|
||||
analyticsEnabled,
|
||||
jwtTTL,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
|
||||
user = {} as jest.Mocked<User>
|
||||
user.roles = Promise.resolve([role])
|
||||
|
||||
userProjector = {} as jest.Mocked<ProjectorInterface<User>>
|
||||
userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' })
|
||||
|
||||
roleProjector = {} as jest.Mocked<ProjectorInterface<Role>>
|
||||
roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' })
|
||||
|
||||
sessionProjector = {} as jest.Mocked<ProjectorInterface<Session>>
|
||||
sessionProjector.projectCustom = jest.fn().mockReturnValue({ foo: 'bar' })
|
||||
sessionProjector.projectSimple = jest.fn().mockReturnValue({ test: 'test' })
|
||||
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<CrossServiceTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 123 })
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user - analytics disabled', async () => {
|
||||
await createUseCase(false).execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user without a session', async () => {
|
||||
await createUseCase().execute({
|
||||
user,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user by user uuid', async () => {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw an error if user does not exist', async () => {
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
|
||||
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
|
||||
|
||||
@injectable()
|
||||
export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@inject(TYPES.RoleProjector) private roleProjector: ProjectorInterface<Role>,
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.ANALYTICS_ENABLED) private analyticsEnabled: boolean,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
|
||||
let user: User | undefined | null = dto.user
|
||||
if (user === undefined && dto.userUuid !== undefined) {
|
||||
user = await this.userRepository.findOneByUuid(dto.userUuid)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with uuid ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: this.projectUser(user),
|
||||
roles: this.projectRoles(roles),
|
||||
}
|
||||
|
||||
if (this.analyticsEnabled) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
authTokenData.analyticsId = analyticsId
|
||||
}
|
||||
|
||||
if (dto.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
|
||||
}
|
||||
}
|
||||
|
||||
private projectUser(user: User): { uuid: string; email: string } {
|
||||
return <{ uuid: string; email: string }>this.userProjector.projectSimple(user)
|
||||
}
|
||||
|
||||
private projectSession(session: Session): {
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
} {
|
||||
return <
|
||||
{
|
||||
uuid: string
|
||||
api_version: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
device_info: string
|
||||
readonly_access: boolean
|
||||
access_expiration: string
|
||||
refresh_expiration: string
|
||||
}
|
||||
>this.sessionProjector.projectSimple(session)
|
||||
}
|
||||
|
||||
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
|
||||
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Either, Uuid } from '@standardnotes/common'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
|
||||
export type CreateCrossServiceTokenDTO = Either<
|
||||
{
|
||||
user: User
|
||||
session?: Session
|
||||
},
|
||||
{
|
||||
userUuid: Uuid
|
||||
}
|
||||
>
|
||||
@@ -0,0 +1,3 @@
|
||||
export type CreateCrossServiceTokenResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -5,17 +5,19 @@ import { TimerInterface } from '@standardnotes/time'
|
||||
import { SubscriptionTokenRepositoryInterface } from '../../Subscription/SubscriptionTokenRepositoryInterface'
|
||||
|
||||
import { CreateSubscriptionToken } from './CreateSubscriptionToken'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('CreateSubscriptionToken', () => {
|
||||
let subscriptionTokenRepository: SubscriptionTokenRepositoryInterface
|
||||
let cryptoNode: CryptoNode
|
||||
let timer: TimerInterface
|
||||
let logger: Logger
|
||||
|
||||
const createUseCase = () => new CreateSubscriptionToken(subscriptionTokenRepository, cryptoNode, timer)
|
||||
const createUseCase = () => new CreateSubscriptionToken(subscriptionTokenRepository, cryptoNode, timer, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionTokenRepository = {} as jest.Mocked<SubscriptionTokenRepositoryInterface>
|
||||
subscriptionTokenRepository.save = jest.fn()
|
||||
subscriptionTokenRepository.save = jest.fn().mockReturnValue(true)
|
||||
|
||||
cryptoNode = {} as jest.Mocked<CryptoNode>
|
||||
cryptoNode.generateRandomKey = jest.fn().mockReturnValueOnce('random-string')
|
||||
@@ -23,6 +25,9 @@ describe('CreateSubscriptionToken', () => {
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertStringDateToMicroseconds = jest.fn().mockReturnValue(1)
|
||||
timer.getUTCDateNHoursAhead = jest.fn().mockReturnValue(new Date(1))
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.error = jest.fn()
|
||||
})
|
||||
|
||||
it('should create an subscription token and persist it', async () => {
|
||||
@@ -36,4 +41,19 @@ describe('CreateSubscriptionToken', () => {
|
||||
expiresAt: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the subscription token was not created', async () => {
|
||||
subscriptionTokenRepository.save = jest.fn().mockReturnValue(false)
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
|
||||
expect(caughtError).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CryptoNode } from '@standardnotes/sncrypto-node'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SubscriptionTokenRepositoryInterface } from '../../Subscription/SubscriptionTokenRepositoryInterface'
|
||||
@@ -15,6 +16,7 @@ export class CreateSubscriptionToken implements UseCaseInterface {
|
||||
private subscriptionTokenRepository: SubscriptionTokenRepositoryInterface,
|
||||
@inject(TYPES.CryptoNode) private cryptoNode: CryptoNode,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateSubscriptionTokenDTO): Promise<CreateSubscriptionTokenResponse> {
|
||||
@@ -26,7 +28,13 @@ export class CreateSubscriptionToken implements UseCaseInterface {
|
||||
expiresAt: this.timer.convertStringDateToMicroseconds(this.timer.getUTCDateNHoursAhead(3).toString()),
|
||||
}
|
||||
|
||||
await this.subscriptionTokenRepository.save(subscriptionToken)
|
||||
const subscriptionTokenWasSaved = await this.subscriptionTokenRepository.save(subscriptionToken)
|
||||
|
||||
if (!subscriptionTokenWasSaved) {
|
||||
this.logger.error(`Could not create subscription token for user ${dto.userUuid}`)
|
||||
|
||||
throw new Error('Could not create subscription token')
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptionToken,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { inject } from 'inversify'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateWebSocketConnectionDTO } from './CreateWebSocketConnectionDTO'
|
||||
import { CreateWebSocketConnectionResponse } from './CreateWebSocketConnectionResponse'
|
||||
|
||||
@injectable()
|
||||
export class CreateWebSocketConnectionToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketConnectionTokenEncoder)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
|
||||
import { SharedSubscriptionInvitation } from '../../SharedSubscription/SharedSubscriptionInvitation'
|
||||
import { InvitationStatus } from '../../SharedSubscription/InvitationStatus'
|
||||
|
||||
describe('InviteToSharedSubscription', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
@@ -187,7 +188,7 @@ describe('InviteToSharedSubscription', () => {
|
||||
it('should not create an invitation if it already exists', async () => {
|
||||
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
|
||||
.fn()
|
||||
.mockReturnValue({} as jest.Mocked<SharedSubscriptionInvitation>)
|
||||
.mockReturnValue({ status: InvitationStatus.Sent } as jest.Mocked<SharedSubscriptionInvitation>)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
@@ -205,4 +206,27 @@ describe('InviteToSharedSubscription', () => {
|
||||
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).not.toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create an invitation if it already exists but was canceled', async () => {
|
||||
sharedSubscriptionInvitationRepository.findOneByInviteeAndInviterEmail = jest
|
||||
.fn()
|
||||
.mockReturnValue({ status: InvitationStatus.Canceled } as jest.Mocked<SharedSubscriptionInvitation>)
|
||||
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
inviteeIdentifier: 'invitee@test.te',
|
||||
inviterUuid: '1-2-3',
|
||||
inviterEmail: 'inviter@test.te',
|
||||
inviterRoles: [RoleName.ProUser],
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(sharedSubscriptionInvitationRepository.save).toHaveBeenCalled()
|
||||
|
||||
expect(domainEventFactory.createSharedSubscriptionInvitationCreatedEvent).toHaveBeenCalled()
|
||||
expect(domainEventPublisher.publish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ export class InviteToSharedSubscription implements UseCaseInterface {
|
||||
dto.inviteeIdentifier,
|
||||
dto.inviterEmail,
|
||||
)
|
||||
if (existingInvitation !== null) {
|
||||
if (existingInvitation !== null && existingInvitation.status !== InvitationStatus.Canceled) {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
|
||||
import { MuteFailedBackupsEmails } from './MuteFailedBackupsEmails'
|
||||
|
||||
describe('MuteFailedBackupsEmails', () => {
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
|
||||
const createUseCase = () => new MuteFailedBackupsEmails(settingRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const setting = {} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(setting)
|
||||
settingRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not succeed if extension setting is not found', async () => {
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update mute email setting on extension setting', async () => {
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
})
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith({
|
||||
value: 'muted',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MuteFailedBackupsEmailsDTO } from './MuteFailedBackupsEmailsDTO'
|
||||
import { MuteFailedBackupsEmailsResponse } from './MuteFailedBackupsEmailsResponse'
|
||||
|
||||
@injectable()
|
||||
export class MuteFailedBackupsEmails implements UseCaseInterface {
|
||||
constructor(@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface) {}
|
||||
|
||||
async execute(dto: MuteFailedBackupsEmailsDTO): Promise<MuteFailedBackupsEmailsResponse> {
|
||||
const setting = await this.settingRepository.findOneByUuidAndNames(dto.settingUuid, [
|
||||
SettingName.MuteFailedBackupsEmails,
|
||||
SettingName.MuteFailedCloudBackupsEmails,
|
||||
])
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
}
|
||||
}
|
||||
|
||||
setting.value = MuteFailedBackupsEmailsOption.Muted
|
||||
|
||||
await this.settingRepository.save(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type MuteFailedBackupsEmailsDTO = {
|
||||
settingUuid: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type MuteFailedBackupsEmailsResponse = {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
|
||||
import { MuteMarketingEmails } from './MuteMarketingEmails'
|
||||
|
||||
describe('MuteMarketingEmails', () => {
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
|
||||
const createUseCase = () => new MuteMarketingEmails(settingRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const setting = {} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(setting)
|
||||
settingRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not succeed if extension setting is not found', async () => {
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update mute email setting on extension setting', async () => {
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
})
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith({
|
||||
value: 'muted',
|
||||
serverEncryptionVersion: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
import { MuteMarketingEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MuteMarketingEmailsDTO } from './MuteMarketingEmailsDTO'
|
||||
import { MuteMarketingEmailsResponse } from './MuteMarketingEmailsResponse'
|
||||
|
||||
@injectable()
|
||||
export class MuteMarketingEmails implements UseCaseInterface {
|
||||
constructor(@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface) {}
|
||||
|
||||
async execute(dto: MuteMarketingEmailsDTO): Promise<MuteMarketingEmailsResponse> {
|
||||
const setting = await this.settingRepository.findOneByUuidAndNames(dto.settingUuid, [
|
||||
SettingName.MuteMarketingEmails,
|
||||
])
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
}
|
||||
}
|
||||
|
||||
setting.value = MuteMarketingEmailsOption.Muted
|
||||
setting.serverEncryptionVersion = EncryptionVersion.Unencrypted
|
||||
|
||||
await this.settingRepository.save(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type MuteMarketingEmailsDTO = {
|
||||
settingUuid: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type MuteMarketingEmailsResponse = {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
|
||||
import { MuteSignInEmails } from './MuteSignInEmails'
|
||||
|
||||
describe('MuteSignInEmails', () => {
|
||||
let settingRepository: SettingRepositoryInterface
|
||||
|
||||
const createUseCase = () => new MuteSignInEmails(settingRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const setting = {} as jest.Mocked<Setting>
|
||||
|
||||
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(setting)
|
||||
settingRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should not succeed if extension setting is not found', async () => {
|
||||
settingRepository.findOneByUuidAndNames = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should update mute email setting on extension setting', async () => {
|
||||
expect(await createUseCase().execute({ settingUuid: '1-2-3' })).toEqual({
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
})
|
||||
|
||||
expect(settingRepository.save).toHaveBeenCalledWith({
|
||||
value: 'muted',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { MuteSignInEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { SettingRepositoryInterface } from '../../Setting/SettingRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { MuteSignInEmailsDTO } from './MuteSignInEmailsDTO'
|
||||
import { MuteSignInEmailsResponse } from './MuteSignInEmailsResponse'
|
||||
|
||||
@injectable()
|
||||
export class MuteSignInEmails implements UseCaseInterface {
|
||||
constructor(@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface) {}
|
||||
|
||||
async execute(dto: MuteSignInEmailsDTO): Promise<MuteSignInEmailsResponse> {
|
||||
const setting = await this.settingRepository.findOneByUuidAndNames(dto.settingUuid, [SettingName.MuteSignInEmails])
|
||||
|
||||
if (setting === null) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Could not find setting setting.',
|
||||
}
|
||||
}
|
||||
|
||||
setting.value = MuteSignInEmailsOption.Muted
|
||||
|
||||
await this.settingRepository.save(setting)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'These emails have been muted.',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type MuteSignInEmailsDTO = {
|
||||
settingUuid: string
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export type MuteSignInEmailsResponse = {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
} from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AddWebSocketsConnection } from '../../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
import { RemoveWebSocketsConnection } from '../../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
|
||||
@controller('/sockets')
|
||||
@@ -18,12 +21,59 @@ export class InversifyExpressWebSocketsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.AddWebSocketsConnection) private addWebSocketsConnection: AddWebSocketsConnection,
|
||||
@inject(TYPES.RemoveWebSocketsConnection) private removeWebSocketsConnection: RemoveWebSocketsConnection,
|
||||
@inject(TYPES.CreateCrossServiceToken) private createCrossServiceToken: CreateCrossServiceToken,
|
||||
@inject(TYPES.WebSocketsController) private webSocketsController: WebSocketServerInterface,
|
||||
@inject(TYPES.WebSocketConnectionTokenDecoder)
|
||||
private tokenDecoder: TokenDecoderInterface<WebSocketConnectionTokenData>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
@httpPost('/tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
async createConnectionToken(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.webSocketsController.createConnectionToken({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result.data, result.status)
|
||||
}
|
||||
|
||||
@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 })
|
||||
}
|
||||
|
||||
@httpPost('/connections/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
async storeWebSocketsConnection(
|
||||
request: Request,
|
||||
response: Response,
|
||||
@@ -36,7 +86,7 @@ export class InversifyExpressWebSocketsController extends BaseHttpController {
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpDelete('/:connectionId')
|
||||
@httpDelete('/connections/:connectionId')
|
||||
async deleteWebSocketsConnection(
|
||||
request: Request,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
@@ -44,13 +94,4 @@ export class InversifyExpressWebSocketsController extends BaseHttpController {
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpPost('/tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
async createConnectionToken(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.webSocketsController.createConnectionToken({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ describe('LockRepository', () => {
|
||||
redisClient.expire = jest.fn()
|
||||
redisClient.del = jest.fn()
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.set = jest.fn()
|
||||
redisClient.setex = jest.fn()
|
||||
})
|
||||
|
||||
@@ -88,6 +87,6 @@ describe('LockRepository', () => {
|
||||
it('should update a lock counter', async () => {
|
||||
await createRepository().updateLockCounter('123', 3)
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('lock:123', 3)
|
||||
expect(redisClient.setex).toHaveBeenCalledWith('lock:123', 120, 3)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LockRepository implements LockRepositoryInterface {
|
||||
}
|
||||
|
||||
async updateLockCounter(userIdentifier: string, counter: number): Promise<void> {
|
||||
await this.redisClient.set(`${this.PREFIX}:${userIdentifier}`, counter)
|
||||
await this.redisClient.setex(`${this.PREFIX}:${userIdentifier}`, this.failedLoginLockout, counter)
|
||||
}
|
||||
|
||||
async getLockCounter(userIdentifier: string): Promise<number> {
|
||||
|
||||
@@ -14,9 +14,9 @@ describe('RedisSubscriptionTokenRepository', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
redisClient = {} as jest.Mocked<IORedis.Redis>
|
||||
redisClient.set = jest.fn()
|
||||
redisClient.set = jest.fn().mockReturnValue('OK')
|
||||
redisClient.get = jest.fn()
|
||||
redisClient.expireat = jest.fn()
|
||||
redisClient.expireat = jest.fn().mockReturnValue(1)
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
|
||||
@@ -45,7 +45,23 @@ describe('RedisSubscriptionTokenRepository', () => {
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
await createRepository().save(subscriptionToken)
|
||||
expect(await createRepository().save(subscriptionToken)).toBeTruthy()
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
|
||||
|
||||
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
|
||||
})
|
||||
|
||||
it('should indicate subscription token was not saved', async () => {
|
||||
redisClient.set = jest.fn().mockReturnValue(null)
|
||||
|
||||
const subscriptionToken: SubscriptionToken = {
|
||||
userUuid: '1-2-3',
|
||||
token: 'random-string',
|
||||
expiresAt: 123,
|
||||
}
|
||||
|
||||
expect(await createRepository().save(subscriptionToken)).toBeFalsy()
|
||||
|
||||
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
|
||||
|
||||
|
||||
@@ -24,11 +24,13 @@ export class RedisSubscriptionTokenRepository implements SubscriptionTokenReposi
|
||||
return userUuid
|
||||
}
|
||||
|
||||
async save(subscriptionToken: SubscriptionToken): Promise<void> {
|
||||
async save(subscriptionToken: SubscriptionToken): Promise<boolean> {
|
||||
const key = `${this.PREFIX}:${subscriptionToken.token}`
|
||||
const expiresAtTimestampInSeconds = this.timer.convertMicrosecondsToSeconds(subscriptionToken.expiresAt)
|
||||
|
||||
await this.redisClient.set(key, subscriptionToken.userUuid)
|
||||
await this.redisClient.expireat(key, expiresAtTimestampInSeconds)
|
||||
const wasSet = await this.redisClient.set(key, subscriptionToken.userUuid)
|
||||
const timeoutWasSet = await this.redisClient.expireat(key, expiresAtTimestampInSeconds)
|
||||
|
||||
return wasSet === 'OK' && timeoutWasSet !== 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,28 +17,31 @@ describe('SettingProjector', () => {
|
||||
serverEncryptionVersion: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<Setting>
|
||||
})
|
||||
|
||||
it('should create a simple projection of a setting', async () => {
|
||||
const projection = await createProjector().projectSimple(setting)
|
||||
expect(projection).toEqual({
|
||||
expect(projection).toStrictEqual({
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
})
|
||||
})
|
||||
it('should create a simple projection of list of settings', async () => {
|
||||
const projection = await createProjector().projectManySimple([setting])
|
||||
expect(projection).toEqual([
|
||||
expect(projection).toStrictEqual([
|
||||
{
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -6,16 +6,16 @@ import { SimpleSetting } from '../Domain/Setting/SimpleSetting'
|
||||
@injectable()
|
||||
export class SettingProjector {
|
||||
async projectSimple(setting: Setting): Promise<SimpleSetting> {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
user,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
serverEncryptionVersion,
|
||||
...rest
|
||||
} = setting
|
||||
|
||||
return rest
|
||||
return {
|
||||
uuid: setting.uuid,
|
||||
name: setting.name,
|
||||
value: setting.value,
|
||||
createdAt: setting.createdAt,
|
||||
updatedAt: setting.updatedAt,
|
||||
sensitive: setting.sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
async projectManySimple(settings: Setting[]): Promise<SimpleSetting[]> {
|
||||
return Promise.all(
|
||||
settings.map(async (setting) => {
|
||||
|
||||
@@ -17,28 +17,31 @@ describe('SubscriptionSettingProjector', () => {
|
||||
serverEncryptionVersion: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
} as jest.Mocked<SubscriptionSetting>
|
||||
})
|
||||
|
||||
it('should create a simple projection of a setting', async () => {
|
||||
const projection = await createProjector().projectSimple(setting)
|
||||
expect(projection).toEqual({
|
||||
expect(projection).toStrictEqual({
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
})
|
||||
})
|
||||
it('should create a simple projection of list of settings', async () => {
|
||||
const projection = await createProjector().projectManySimple([setting])
|
||||
expect(projection).toEqual([
|
||||
expect(projection).toStrictEqual([
|
||||
{
|
||||
uuid: 'setting-uuid',
|
||||
name: 'setting-name',
|
||||
value: 'setting-value',
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
sensitive: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -6,16 +6,16 @@ import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
|
||||
@injectable()
|
||||
export class SubscriptionSettingProjector {
|
||||
async projectSimple(setting: SubscriptionSetting): Promise<SimpleSubscriptionSetting> {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
userSubscription,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
serverEncryptionVersion,
|
||||
...rest
|
||||
} = setting
|
||||
|
||||
return rest
|
||||
return {
|
||||
uuid: setting.uuid,
|
||||
name: setting.name,
|
||||
value: setting.value,
|
||||
createdAt: setting.createdAt,
|
||||
updatedAt: setting.updatedAt,
|
||||
sensitive: setting.sensitive,
|
||||
}
|
||||
}
|
||||
|
||||
async projectManySimple(settings: SubscriptionSetting[]): Promise<SimpleSubscriptionSetting[]> {
|
||||
return Promise.all(
|
||||
settings.map(async (setting) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [1.8.13](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.12...@standardnotes/domain-events-infra@1.8.13) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.8.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.11...@standardnotes/domain-events-infra@1.8.12) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.8.12",
|
||||
"version": "1.8.13",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.60.7](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.6...@standardnotes/domain-events@2.60.7) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [2.60.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.5...@standardnotes/domain-events@2.60.6) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.60.6",
|
||||
"version": "2.60.7",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/features": "^1.47.0",
|
||||
"@standardnotes/features": "^1.52.1",
|
||||
"@standardnotes/predicates": "workspace:*",
|
||||
"@standardnotes/security": "workspace:*",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [1.3.18](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.17...@standardnotes/event-store@1.3.18) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.3.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.16...@standardnotes/event-store@1.3.17) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.3.17",
|
||||
"version": "1.3.18",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [1.6.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.3...@standardnotes/files-server@1.6.4) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.6.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.2...@standardnotes/files-server@1.6.3) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.6.3",
|
||||
"version": "1.6.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [1.10.32](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.31...@standardnotes/scheduler-server@1.10.32) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.10.31](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.30...@standardnotes/scheduler-server@1.10.31) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.10.31",
|
||||
"version": "1.10.32",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.8.11](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.10...@standardnotes/syncing-server@1.8.11) (2022-09-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.8.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.9...@standardnotes/syncing-server@1.8.10) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server-js:** binding of sync limit ([5628de6](https://github.com/standardnotes/syncing-server-js/commit/5628de6445a90901735b449488fb8c8374f2171e))
|
||||
|
||||
## [1.8.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.8...@standardnotes/syncing-server@1.8.9) (2022-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** introduce upper bound for sync items limit as an env var ([03d1bc6](https://github.com/standardnotes/syncing-server-js/commit/03d1bc611c39c0d1f0bcaa00824825304e08d30b))
|
||||
|
||||
## [1.8.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.7...@standardnotes/syncing-server@1.8.8) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.8.8",
|
||||
"version": "1.8.11",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -90,6 +90,7 @@ const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
|
||||
private readonly DEFAULT_MAX_ITEMS_LIMIT = 300
|
||||
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
@@ -191,7 +192,16 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
|
||||
container
|
||||
.bind(TYPES.CONTENT_SIZE_TRANSFER_LIMIT)
|
||||
.toConstantValue(env.get('CONTENT_SIZE_TRANSFER_LIMIT', true) ?? this.DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT)
|
||||
.toConstantValue(
|
||||
env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
|
||||
? +env.get('CONTENT_SIZE_TRANSFER_LIMIT', true)
|
||||
: this.DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT,
|
||||
)
|
||||
container
|
||||
.bind(TYPES.MAX_ITEMS_LIMIT)
|
||||
.toConstantValue(
|
||||
env.get('MAX_ITEMS_LIMIT', true) ? +env.get('MAX_ITEMS_LIMIT', true) : this.DEFAULT_MAX_ITEMS_LIMIT,
|
||||
)
|
||||
|
||||
// use cases
|
||||
container.bind<SyncItems>(TYPES.SyncItems).to(SyncItems)
|
||||
|
||||
@@ -36,6 +36,7 @@ const TYPES = {
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
VERSION: Symbol.for('VERSION'),
|
||||
CONTENT_SIZE_TRANSFER_LIMIT: Symbol.for('CONTENT_SIZE_TRANSFER_LIMIT'),
|
||||
MAX_ITEMS_LIMIT: Symbol.for('MAX_ITEMS_LIMIT'),
|
||||
// use cases
|
||||
SyncItems: Symbol.for('SyncItems'),
|
||||
CheckIntegrity: Symbol.for('CheckIntegrity'),
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('ItemService', () => {
|
||||
let timeHelper: Timer
|
||||
let itemTransferCalculator: ItemTransferCalculatorInterface
|
||||
let itemProjector: ProjectorInterface<Item, ItemProjection>
|
||||
const maxItemsSyncLimit = 300
|
||||
|
||||
const createService = () =>
|
||||
new ItemService(
|
||||
@@ -54,6 +55,7 @@ describe('ItemService', () => {
|
||||
itemTransferCalculator,
|
||||
timer,
|
||||
itemProjector,
|
||||
maxItemsSyncLimit,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import { ItemProjection } from '../../Projection/ItemProjection'
|
||||
@injectable()
|
||||
export class ItemService implements ItemServiceInterface {
|
||||
private readonly DEFAULT_ITEMS_LIMIT = 150
|
||||
private readonly MAX_ITEMS_LIMIT = 300
|
||||
private readonly SYNC_TOKEN_VERSION = 2
|
||||
|
||||
constructor(
|
||||
@@ -42,6 +41,7 @@ export class ItemService implements ItemServiceInterface {
|
||||
@inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
|
||||
@inject(TYPES.MAX_ITEMS_LIMIT) private maxItemsSyncLimit: number,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class ItemService implements ItemServiceInterface {
|
||||
deleted: lastSyncTime ? undefined : false,
|
||||
sortBy: 'updated_at_timestamp',
|
||||
sortOrder: 'ASC',
|
||||
limit: limit < this.MAX_ITEMS_LIMIT ? limit : this.MAX_ITEMS_LIMIT,
|
||||
limit: limit < this.maxItemsSyncLimit ? limit : this.maxItemsSyncLimit,
|
||||
}
|
||||
|
||||
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -1829,7 +1829,7 @@ __metadata:
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
"@standardnotes/domain-events-infra": "workspace:*"
|
||||
"@standardnotes/features": ^1.47.0
|
||||
"@standardnotes/features": ^1.52.1
|
||||
"@standardnotes/predicates": "workspace:*"
|
||||
"@standardnotes/responses": ^1.6.39
|
||||
"@standardnotes/security": "workspace:*"
|
||||
@@ -1939,7 +1939,7 @@ __metadata:
|
||||
resolution: "@standardnotes/domain-events@workspace:packages/domain-events"
|
||||
dependencies:
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/features": ^1.47.0
|
||||
"@standardnotes/features": ^1.52.1
|
||||
"@standardnotes/predicates": "workspace:*"
|
||||
"@standardnotes/security": "workspace:*"
|
||||
"@types/jest": ^28.1.4
|
||||
@@ -2016,6 +2016,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@standardnotes/features@npm:^1.52.1":
|
||||
version: 1.52.1
|
||||
resolution: "@standardnotes/features@npm:1.52.1"
|
||||
dependencies:
|
||||
"@standardnotes/auth": ^3.19.4
|
||||
"@standardnotes/common": ^1.32.0
|
||||
"@standardnotes/security": ^1.2.0
|
||||
reflect-metadata: ^0.1.13
|
||||
checksum: ff3684399e0e0c0e799f11e69dddea2e1f65f315e5a5dd3ca5640e24e836ee85faf1f5ee15fc804411bf083004527fcef08411d5c2d0b5894491bf2f28ceca68
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@standardnotes/files-server@workspace:packages/files":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@standardnotes/files-server@workspace:packages/files"
|
||||
|
||||
Reference in New Issue
Block a user