feat(auth): remove websocket handling in favor of websockets service

This commit is contained in:
Karol Sójko 2022-10-13 12:23:08 +02:00
parent 863e8555ae
commit 86ae4a59a3
No known key found for this signature in database
GPG key ID: A50543BF560BDEB0
31 changed files with 81 additions and 457 deletions

View file

@ -7,6 +7,7 @@ PORT=3000
SYNCING_SERVER_JS_URL=http://syncing_server_js:3000
AUTH_SERVER_URL=http://auth:3000
WORKSPACE_SERVER_URL=http://workspace:3000
WEB_SOCKET_SERVER_URL=http://websockets:3000
PAYMENTS_SERVER_URL=http://payments:3000
FILES_SERVER_URL=http://files:3000

View file

@ -76,6 +76,7 @@ export class ContainerConfigLoader {
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
container.bind(TYPES.WORKSPACE_SERVER_URL).toConstantValue(env.get('WORKSPACE_SERVER_URL'))
container.bind(TYPES.WEB_SOCKET_SERVER_URL).toConstantValue(env.get('WEB_SOCKET_SERVER_URL'))
container
.bind(TYPES.HTTP_CALL_TIMEOUT)
.toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000)

View file

@ -9,6 +9,7 @@ const TYPES = {
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
HTTP_CALL_TIMEOUT: Symbol.for('HTTP_CALL_TIMEOUT'),
VERSION: Symbol.for('VERSION'),

View file

@ -17,7 +17,7 @@ export class WebSocketsController extends BaseHttpController {
@httpPost('/tokens', TYPES.AuthMiddleware)
async createWebSocketConnectionToken(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'sockets/tokens', request.body)
await this.httpService.callWebSocketServer(request, response, 'sockets/tokens', request.body)
}
@httpPost('/connections', TYPES.WebSocketAuthMiddleware)
@ -30,7 +30,7 @@ export class WebSocketsController extends BaseHttpController {
return
}
await this.httpService.callAuthServer(
await this.httpService.callWebSocketServer(
request,
response,
`sockets/connections/${request.headers.connectionid}`,
@ -48,7 +48,7 @@ export class WebSocketsController extends BaseHttpController {
return
}
await this.httpService.callAuthServer(
await this.httpService.callWebSocketServer(
request,
response,
`sockets/connections/${request.headers.connectionid}`,

View file

@ -17,6 +17,7 @@ export class HttpService implements HttpServiceInterface {
@inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
@inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
@inject(TYPES.WORKSPACE_SERVER_URL) private workspaceServerUrl: string,
@inject(TYPES.WEB_SOCKET_SERVER_URL) private webSocketServerUrl: string,
@inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
@inject(TYPES.Logger) private logger: Logger,
@ -58,6 +59,15 @@ export class HttpService implements HttpServiceInterface {
await this.callServer(this.workspaceServerUrl, request, response, endpoint, payload)
}
async callWebSocketServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void> {
await this.callServer(this.webSocketServerUrl, request, response, endpoint, payload)
}
async callPaymentsServer(
request: Request,
response: Response,

View file

@ -37,4 +37,10 @@ export interface HttpServiceInterface {
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
callWebSocketServer(
request: Request,
response: Response,
endpoint: string,
payload?: Record<string, unknown> | string,
): Promise<void>
}

View file

@ -67,7 +67,6 @@ VALET_TOKEN_SECRET=
VALET_TOKEN_TTL=
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
WEB_SOCKET_CONNECTION_TOKEN_TTL=
# (Optional) Analytics
ANALYTICS_ENABLED=false

View file

@ -79,10 +79,6 @@ import { DeleteAccount } from '../Domain/UseCase/DeleteAccount/DeleteAccount'
import { DeleteSetting } from '../Domain/UseCase/DeleteSetting/DeleteSetting'
import { SettingFactory } from '../Domain/Setting/SettingFactory'
import { SettingService } from '../Domain/Setting/SettingService'
import { WebSocketsConnectionRepositoryInterface } from '../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
import { RedisWebSocketsConnectionRepository } from '../Infra/Redis/RedisWebSocketsConnectionRepository'
import { AddWebSocketsConnection } from '../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
import { RemoveWebSocketsConnection } from '../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
import axios, { AxiosInstance } from 'axios'
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
import { MySQLUserSubscriptionRepository } from '../Infra/MySQL/MySQLUserSubscriptionRepository'
@ -206,9 +202,6 @@ import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventH
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
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
@ -273,7 +266,6 @@ export class ContainerConfigLoader {
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
container.bind<WebSocketServerInterface>(TYPES.WebSocketsController).to(WebSocketsController)
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
@ -295,9 +287,6 @@ export class ContainerConfigLoader {
.bind<RedisEphemeralSessionRepository>(TYPES.EphemeralSessionRepository)
.to(RedisEphemeralSessionRepository)
container.bind<LockRepository>(TYPES.LockRepository).to(LockRepository)
container
.bind<WebSocketsConnectionRepositoryInterface>(TYPES.WebSocketsConnectionRepository)
.to(RedisWebSocketsConnectionRepository)
container
.bind<SubscriptionTokenRepositoryInterface>(TYPES.SubscriptionTokenRepository)
.to(RedisSubscriptionTokenRepository)
@ -375,9 +364,6 @@ export class ContainerConfigLoader {
container
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)
.toConstantValue(env.get('WEB_SOCKET_CONNECTION_TOKEN_SECRET', true))
container
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL)
.toConstantValue(+env.get('WEB_SOCKET_CONNECTION_TOKEN_TTL', true))
container.bind(TYPES.ENCRYPTION_SERVER_KEY).toConstantValue(env.get('ENCRYPTION_SERVER_KEY'))
container.bind(TYPES.ACCESS_TOKEN_AGE).toConstantValue(env.get('ACCESS_TOKEN_AGE'))
container.bind(TYPES.REFRESH_TOKEN_AGE).toConstantValue(env.get('REFRESH_TOKEN_AGE'))
@ -399,7 +385,6 @@ export class ContainerConfigLoader {
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.SYNCING_SERVER_URL).toConstantValue(env.get('SYNCING_SERVER_URL'))
container.bind(TYPES.WEBSOCKETS_API_URL).toConstantValue(env.get('WEBSOCKETS_API_URL', true))
container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
@ -424,8 +409,6 @@ export class ContainerConfigLoader {
container.bind<UpdateSetting>(TYPES.UpdateSetting).to(UpdateSetting)
container.bind<DeleteSetting>(TYPES.DeleteSetting).to(DeleteSetting)
container.bind<DeleteAccount>(TYPES.DeleteAccount).to(DeleteAccount)
container.bind<AddWebSocketsConnection>(TYPES.AddWebSocketsConnection).to(AddWebSocketsConnection)
container.bind<RemoveWebSocketsConnection>(TYPES.RemoveWebSocketsConnection).to(RemoveWebSocketsConnection)
container.bind<GetUserSubscription>(TYPES.GetUserSubscription).to(GetUserSubscription)
container.bind<GetUserOfflineSubscription>(TYPES.GetUserOfflineSubscription).to(GetUserOfflineSubscription)
container.bind<CreateSubscriptionToken>(TYPES.CreateSubscriptionToken).to(CreateSubscriptionToken)
@ -454,9 +437,6 @@ export class ContainerConfigLoader {
container.bind<GetSubscriptionSetting>(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting)
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
container
.bind<CreateWebSocketConnectionToken>(TYPES.CreateWebSocketConnectionToken)
.to(CreateWebSocketConnectionToken)
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
// Handlers
@ -547,11 +527,6 @@ export class ContainerConfigLoader {
container
.bind<TokenEncoderInterface<ValetTokenData>>(TYPES.ValetTokenEncoder)
.toConstantValue(new TokenEncoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
container
.bind<TokenEncoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenEncoder)
.toConstantValue(
new TokenEncoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
)
container.bind<AuthenticationMethodResolver>(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver)
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())

View file

@ -6,7 +6,6 @@ const TYPES = {
// Controller
AuthController: Symbol.for('AuthController'),
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
WebSocketsController: Symbol.for('WebSocketsController'),
// Repositories
UserRepository: Symbol.for('UserRepository'),
SessionRepository: Symbol.for('SessionRepository'),
@ -17,7 +16,6 @@ const TYPES = {
OfflineSettingRepository: Symbol.for('OfflineSettingRepository'),
LockRepository: Symbol.for('LockRepository'),
RoleRepository: Symbol.for('RoleRepository'),
WebSocketsConnectionRepository: Symbol.for('WebSocketsConnectionRepository'),
UserSubscriptionRepository: Symbol.for('UserSubscriptionRepository'),
OfflineUserSubscriptionRepository: Symbol.for('OfflineUserSubscriptionRepository'),
SubscriptionTokenRepository: Symbol.for('SubscriptionTokenRepository'),
@ -83,7 +81,6 @@ const TYPES = {
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
SYNCING_SERVER_URL: Symbol.for('SYNCING_SERVER_URL'),
WEBSOCKETS_API_URL: Symbol.for('WEBSOCKETS_API_URL'),
VERSION: Symbol.for('VERSION'),
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
// use cases
@ -107,8 +104,6 @@ const TYPES = {
UpdateSetting: Symbol.for('UpdateSetting'),
DeleteSetting: Symbol.for('DeleteSetting'),
DeleteAccount: Symbol.for('DeleteAccount'),
AddWebSocketsConnection: Symbol.for('AddWebSocketsConnection'),
RemoveWebSocketsConnection: Symbol.for('RemoveWebSocketsConnection'),
GetUserSubscription: Symbol.for('GetUserSubscription'),
GetUserOfflineSubscription: Symbol.for('GetUserOfflineSubscription'),
CreateSubscriptionToken: Symbol.for('CreateSubscriptionToken'),
@ -125,7 +120,6 @@ const TYPES = {
GetSubscriptionSetting: Symbol.for('GetSubscriptionSetting'),
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
VerifyPredicate: Symbol.for('VerifyPredicate'),
CreateWebSocketConnectionToken: Symbol.for('CreateWebSocketConnectionToken'),
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
@ -168,7 +162,6 @@ const TYPES = {
CrossServiceTokenEncoder: Symbol.for('CrossServiceTokenEncoder'),
SessionTokenEncoder: Symbol.for('SessionTokenEncoder'),
ValetTokenEncoder: Symbol.for('ValetTokenEncoder'),
WebSocketConnectionTokenEncoder: Symbol.for('WebSocketConnectionTokenEncoder'),
WebSocketConnectionTokenDecoder: Symbol.for('WebSocketConnectionTokenDecoder'),
AuthenticationMethodResolver: Symbol.for('AuthenticationMethodResolver'),
DomainEventPublisher: Symbol.for('DomainEventPublisher'),

View file

@ -1,28 +0,0 @@
import 'reflect-metadata'
import { WebSocketsController } from './WebSocketsController'
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
describe('WebSocketsController', () => {
let createWebSocketConnectionToken: CreateWebSocketConnectionToken
const createController = () => new WebSocketsController(createWebSocketConnectionToken)
beforeEach(() => {
createWebSocketConnectionToken = {} as jest.Mocked<CreateWebSocketConnectionToken>
createWebSocketConnectionToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
})
it('should create a web sockets connection token', async () => {
const response = await createController().createConnectionToken({ userUuid: '1-2-3' })
expect(response).toEqual({
status: 200,
data: { token: 'foobar' },
})
expect(createWebSocketConnectionToken.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
})
})
})

View file

@ -1,29 +0,0 @@
import {
HttpStatusCode,
WebSocketConnectionTokenRequestParams,
WebSocketConnectionTokenResponse,
WebSocketServerInterface,
} from '@standardnotes/api'
import { inject, injectable } from 'inversify'
import TYPES from '../Bootstrap/Types'
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
@injectable()
export class WebSocketsController implements WebSocketServerInterface {
constructor(
@inject(TYPES.CreateWebSocketConnectionToken)
private createWebSocketConnectionToken: CreateWebSocketConnectionToken,
) {}
async createConnectionToken(
params: WebSocketConnectionTokenRequestParams,
): Promise<WebSocketConnectionTokenResponse> {
const result = await this.createWebSocketConnectionToken.execute({ userUuid: params.userUuid as string })
return {
status: HttpStatusCode.Success,
data: result,
}
}
}

View file

@ -18,6 +18,29 @@ describe('DomainEventFactory', () => {
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
})
it('should create a WEB_SOCKET_MESSAGE_REQUESTED event', () => {
expect(
createFactory().createWebSocketMessageRequestedEvent({
userUuid: '1-2-3',
message: 'foobar',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '1-2-3',
userIdentifierType: 'uuid',
},
origin: 'auth',
},
payload: {
userUuid: '1-2-3',
message: 'foobar',
},
type: 'WEB_SOCKET_MESSAGE_REQUESTED',
})
})
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
expect(
createFactory().createEmailMessageRequestedEvent({

View file

@ -1,4 +1,4 @@
import { EmailMessageIdentifier, ProtocolVersion, RoleName, Uuid } from '@standardnotes/common'
import { EmailMessageIdentifier, JSONString, ProtocolVersion, RoleName, Uuid } from '@standardnotes/common'
import {
AccountDeletionRequestedEvent,
UserEmailChangedEvent,
@ -15,6 +15,7 @@ import {
PredicateVerifiedEvent,
DomainEventService,
EmailMessageRequestedEvent,
WebSocketMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@ -27,6 +28,21 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createWebSocketMessageRequestedEvent(dto: { userUuid: Uuid; message: JSONString }): WebSocketMessageRequestedEvent {
return {
type: 'WEB_SOCKET_MESSAGE_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createEmailMessageRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier

View file

@ -1,4 +1,4 @@
import { Uuid, RoleName, EmailMessageIdentifier, ProtocolVersion } from '@standardnotes/common'
import { Uuid, RoleName, EmailMessageIdentifier, ProtocolVersion, JSONString } from '@standardnotes/common'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import {
AccountDeletionRequestedEvent,
@ -15,10 +15,12 @@ import {
SharedSubscriptionInvitationCanceledEvent,
PredicateVerifiedEvent,
EmailMessageRequestedEvent,
WebSocketMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
export interface DomainEventFactoryInterface {
createWebSocketMessageRequestedEvent(dto: { userUuid: Uuid; message: JSONString }): WebSocketMessageRequestedEvent
createEmailMessageRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier

View file

@ -1,26 +0,0 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { AddWebSocketsConnection } from './AddWebSocketsConnection'
describe('AddWebSocketsConnection', () => {
let webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface
let logger: Logger
const createUseCase = () => new AddWebSocketsConnection(webSocketsConnectionRepository, logger)
beforeEach(() => {
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
webSocketsConnectionRepository.saveConnection = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
})
it('should save a web sockets connection for a user for further communication', async () => {
await createUseCase().execute({ userUuid: '1-2-3', connectionId: '2-3-4' })
expect(webSocketsConnectionRepository.saveConnection).toHaveBeenCalledWith('1-2-3', '2-3-4')
})
})

View file

@ -1,26 +0,0 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { AddWebSocketsConnectionDTO } from './AddWebSocketsConnectionDTO'
import { AddWebSocketsConnectionResponse } from './AddWebSocketsConnectionResponse'
@injectable()
export class AddWebSocketsConnection implements UseCaseInterface {
constructor(
@inject(TYPES.WebSocketsConnectionRepository)
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: AddWebSocketsConnectionDTO): Promise<AddWebSocketsConnectionResponse> {
this.logger.debug(`Persisting connection ${dto.connectionId} for user ${dto.userUuid}`)
await this.webSocketsConnectionRepository.saveConnection(dto.userUuid, dto.connectionId)
return {
success: true,
}
}
}

View file

@ -1,4 +0,0 @@
export type AddWebSocketsConnectionDTO = {
userUuid: string
connectionId: string
}

View file

@ -1,3 +0,0 @@
export type AddWebSocketsConnectionResponse = {
success: boolean
}

View file

@ -1,25 +0,0 @@
import 'reflect-metadata'
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
import { CreateWebSocketConnectionToken } from './CreateWebSocketConnectionToken'
describe('CreateWebSocketConnection', () => {
let tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>
const tokenTTL = 30
const createUseCase = () => new CreateWebSocketConnectionToken(tokenEncoder, tokenTTL)
beforeEach(() => {
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<WebSocketConnectionTokenData>>
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
})
it('should create a web socket connection token', async () => {
const result = await createUseCase().execute({ userUuid: '1-2-3' })
expect(result.token).toEqual('foobar')
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith({ userUuid: '1-2-3' }, 30)
})
})

View file

@ -1,3 +0,0 @@
export type CreateWebSocketConnectionDTO = {
userUuid: string
}

View file

@ -1,3 +0,0 @@
export type CreateWebSocketConnectionResponse = {
token: string
}

View file

@ -1,26 +0,0 @@
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
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)
private tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>,
@inject(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL) private tokenTTL: number,
) {}
async execute(dto: CreateWebSocketConnectionDTO): Promise<CreateWebSocketConnectionResponse> {
const data: WebSocketConnectionTokenData = {
userUuid: dto.userUuid,
}
return {
token: this.tokenEncoder.encodeExpirableToken(data, this.tokenTTL),
}
}
}

View file

@ -1,26 +0,0 @@
import 'reflect-metadata'
import { Logger } from 'winston'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { RemoveWebSocketsConnection } from './RemoveWebSocketsConnection'
describe('RemoveWebSocketsConnection', () => {
let webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface
let logger: Logger
const createUseCase = () => new RemoveWebSocketsConnection(webSocketsConnectionRepository, logger)
beforeEach(() => {
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
webSocketsConnectionRepository.removeConnection = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
})
it('should remove a web sockets connection', async () => {
await createUseCase().execute({ connectionId: '2-3-4' })
expect(webSocketsConnectionRepository.removeConnection).toHaveBeenCalledWith('2-3-4')
})
})

View file

@ -1,26 +0,0 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { RemoveWebSocketsConnectionDTO } from './RemoveWebSocketsConnectionDTO'
import { RemoveWebSocketsConnectionResponse } from './RemoveWebSocketsConnectionResponse'
@injectable()
export class RemoveWebSocketsConnection implements UseCaseInterface {
constructor(
@inject(TYPES.WebSocketsConnectionRepository)
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: RemoveWebSocketsConnectionDTO): Promise<RemoveWebSocketsConnectionResponse> {
this.logger.debug(`Removing connection ${dto.connectionId}`)
await this.webSocketsConnectionRepository.removeConnection(dto.connectionId)
return {
success: true,
}
}
}

View file

@ -1,3 +0,0 @@
export type RemoveWebSocketsConnectionDTO = {
connectionId: string
}

View file

@ -1,3 +0,0 @@
export type RemoveWebSocketsConnectionResponse = {
success: boolean
}

View file

@ -1,43 +1,27 @@
import { WebSocketServerInterface } from '@standardnotes/api'
import { ErrorTag } from '@standardnotes/common'
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
import { Request, Response } from 'express'
import { Request } from 'express'
import { inject } from 'inversify'
import {
BaseHttpController,
controller,
httpDelete,
httpPost,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
results,
} 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')
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('/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) {
@ -72,26 +56,4 @@ export class InversifyExpressWebSocketsController extends BaseHttpController {
return this.json({ authToken: result.token })
}
@httpPost('/connections/:connectionId', TYPES.ApiGatewayAuthMiddleware)
async storeWebSocketsConnection(
request: Request,
response: Response,
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
await this.addWebSocketsConnection.execute({
userUuid: response.locals.user.uuid,
connectionId: request.params.connectionId,
})
return this.json({ success: true })
}
@httpDelete('/connections/:connectionId')
async deleteWebSocketsConnection(
request: Request,
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
return this.json({ success: true })
}
}

View file

@ -1,44 +0,0 @@
import 'reflect-metadata'
import * as IORedis from 'ioredis'
import { RedisWebSocketsConnectionRepository } from './RedisWebSocketsConnectionRepository'
describe('RedisWebSocketsConnectionRepository', () => {
let redisClient: IORedis.Redis
const createRepository = () => new RedisWebSocketsConnectionRepository(redisClient)
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.sadd = jest.fn()
redisClient.set = jest.fn()
redisClient.get = jest.fn()
redisClient.srem = jest.fn()
redisClient.del = jest.fn()
redisClient.smembers = jest.fn()
})
it('should save a connection to set of user connections', async () => {
await createRepository().saveConnection('1-2-3', '2-3-4')
expect(redisClient.sadd).toHaveBeenCalledWith('ws_user_connections:1-2-3', '2-3-4')
expect(redisClient.set).toHaveBeenCalledWith('ws_connection:2-3-4', '1-2-3')
})
it('should remove a connection from the set of user connections', async () => {
redisClient.get = jest.fn().mockReturnValue('1-2-3')
await createRepository().removeConnection('2-3-4')
expect(redisClient.srem).toHaveBeenCalledWith('ws_user_connections:1-2-3', '2-3-4')
expect(redisClient.del).toHaveBeenCalledWith('ws_connection:2-3-4')
})
it('should return all connections for a user uuid', async () => {
const userUuid = '1-2-3'
await createRepository().findAllByUserUuid(userUuid)
expect(redisClient.smembers).toHaveBeenCalledWith(`ws_user_connections:${userUuid}`)
})
})

View file

@ -1,28 +0,0 @@
import * as IORedis from 'ioredis'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { WebSocketsConnectionRepositoryInterface } from '../../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
@injectable()
export class RedisWebSocketsConnectionRepository implements WebSocketsConnectionRepositoryInterface {
private readonly WEB_SOCKETS_USER_CONNECTIONS_PREFIX = 'ws_user_connections'
private readonly WEB_SOCKETS_CONNETION_PREFIX = 'ws_connection'
constructor(@inject(TYPES.Redis) private redisClient: IORedis.Redis) {}
async findAllByUserUuid(userUuid: string): Promise<string[]> {
return await this.redisClient.smembers(`${this.WEB_SOCKETS_USER_CONNECTIONS_PREFIX}:${userUuid}`)
}
async removeConnection(connectionId: string): Promise<void> {
const userUuid = await this.redisClient.get(`${this.WEB_SOCKETS_CONNETION_PREFIX}:${connectionId}`)
await this.redisClient.srem(`${this.WEB_SOCKETS_USER_CONNECTIONS_PREFIX}:${userUuid}`, connectionId)
await this.redisClient.del(`${this.WEB_SOCKETS_CONNETION_PREFIX}:${connectionId}`)
}
async saveConnection(userUuid: string, connectionId: string): Promise<void> {
await this.redisClient.set(`${this.WEB_SOCKETS_CONNETION_PREFIX}:${connectionId}`, userUuid)
await this.redisClient.sadd(`${this.WEB_SOCKETS_USER_CONNECTIONS_PREFIX}:${userUuid}`, connectionId)
}
}

View file

@ -1,38 +1,21 @@
import 'reflect-metadata'
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
import { DomainEventPublisherInterface, UserRolesChangedEvent } from '@standardnotes/domain-events'
import { RoleName } from '@standardnotes/common'
import { User } from '../../Domain/User/User'
import { WebSocketsClientService } from './WebSocketsClientService'
import { WebSocketsConnectionRepositoryInterface } from '../../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
import { DomainEventFactoryInterface } from '../../Domain/Event/DomainEventFactoryInterface'
import { AxiosInstance } from 'axios'
import { Logger } from 'winston'
describe('WebSocketsClientService', () => {
let connectionIds: string[]
let user: User
let event: UserRolesChangedEvent
let webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface
let domainEventFactory: DomainEventFactoryInterface
let httpClient: AxiosInstance
let logger: Logger
let domainEventPublisher: DomainEventPublisherInterface
let webSocketsApiUrl = 'http://test-websockets'
const createService = () =>
new WebSocketsClientService(
webSocketsConnectionRepository,
domainEventFactory,
httpClient,
webSocketsApiUrl,
logger,
)
const createService = () => new WebSocketsClientService(domainEventFactory, domainEventPublisher)
beforeEach(() => {
connectionIds = ['1', '2']
user = {
uuid: '123',
email: 'test@test.com',
@ -45,43 +28,19 @@ describe('WebSocketsClientService', () => {
event = {} as jest.Mocked<UserRolesChangedEvent>
webSocketsConnectionRepository = {} as jest.Mocked<WebSocketsConnectionRepositoryInterface>
webSocketsConnectionRepository.findAllByUserUuid = jest.fn().mockReturnValue(connectionIds)
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createUserRolesChangedEvent = jest.fn().mockReturnValue(event)
httpClient = {} as jest.Mocked<AxiosInstance>
httpClient.request = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
})
it('should send a user role changed event to all user connections', async () => {
it('should request a message about a user role changed', async () => {
await createService().sendUserRolesChangedEvent(user)
expect(domainEventFactory.createUserRolesChangedEvent).toHaveBeenCalledWith('123', 'test@test.com', [
RoleName.ProUser,
])
expect(httpClient.request).toHaveBeenCalledTimes(connectionIds.length)
connectionIds.map((id, index) => {
expect(httpClient.request).toHaveBeenNthCalledWith(
index + 1,
expect.objectContaining({
method: 'POST',
url: `${webSocketsApiUrl}/${id}`,
data: JSON.stringify(event),
}),
)
})
})
it('should not send a user role changed event if web sockets api url not defined', async () => {
webSocketsApiUrl = ''
await createService().sendUserRolesChangedEvent(user)
expect(httpClient.request).not.toHaveBeenCalled()
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
})

View file

@ -1,52 +1,31 @@
import { AxiosInstance } from 'axios'
import { RoleName } from '@standardnotes/common'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { DomainEventFactoryInterface } from '../../Domain/Event/DomainEventFactoryInterface'
import { User } from '../../Domain/User/User'
import { WebSocketsConnectionRepositoryInterface } from '../../Domain/WebSockets/WebSocketsConnectionRepositoryInterface'
import { ClientServiceInterface } from '../../Domain/Client/ClientServiceInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
@injectable()
export class WebSocketsClientService implements ClientServiceInterface {
constructor(
@inject(TYPES.WebSocketsConnectionRepository)
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
@inject(TYPES.WEBSOCKETS_API_URL) private webSocketsApiUrl: string,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
) {}
async sendUserRolesChangedEvent(user: User): Promise<void> {
if (!this.webSocketsApiUrl) {
this.logger.debug('Web Sockets API url not defined. Skipped sending user role changed event.')
return
}
const userConnections = await this.webSocketsConnectionRepository.findAllByUserUuid(user.uuid)
const event = this.domainEventFactory.createUserRolesChangedEvent(
user.uuid,
user.email,
(await user.roles).map((role) => role.name) as RoleName[],
)
for (const connectionUuid of userConnections) {
await this.httpClient.request({
method: 'POST',
url: `${this.webSocketsApiUrl}/${connectionUuid}`,
headers: {
Accept: 'text/plain',
'Content-Type': 'text/plain',
},
data: JSON.stringify(event),
validateStatus:
/* istanbul ignore next */
(status: number) => status >= 200 && status < 500,
})
}
await this.domainEventPublisher.publish(
this.domainEventFactory.createWebSocketMessageRequestedEvent({
userUuid: user.uuid,
message: JSON.stringify(event),
}),
)
}
}