Compare commits

...

10 Commits

Author SHA1 Message Date
standardci
79c00b0e7a chore(release): publish new version
- @standardnotes/api-gateway@1.31.2
2022-10-13 11:11:09 +00:00
Karol Sójko
219b1baa41 fix(api-gateway): make web sockets url optional 2022-10-13 13:09:18 +02:00
standardci
c0cb7f7a92 chore(release): publish new version
- @standardnotes/api-gateway@1.31.1
 - @standardnotes/auth-server@1.45.0
 - @standardnotes/domain-events-infra@1.8.27
 - @standardnotes/domain-events@2.68.0
 - @standardnotes/event-store@1.4.6
 - @standardnotes/files-server@1.6.18
 - @standardnotes/scheduler-server@1.10.46
 - @standardnotes/syncing-server@1.9.8
 - @standardnotes/websockets-server@1.1.3
 - @standardnotes/workspace-server@1.14.0
2022-10-13 10:52:01 +00:00
Karol Sójko
86379eb96d feat: publish workspace invite accepted event for websockets 2022-10-13 12:50:10 +02:00
standardci
f7762a97e3 chore(release): publish new version
- @standardnotes/api-gateway@1.31.0
 - @standardnotes/auth-server@1.44.0
2022-10-13 10:25:22 +00:00
Karol Sójko
86ae4a59a3 feat(auth): remove websocket handling in favor of websockets service 2022-10-13 12:23:14 +02:00
standardci
863e8555ae chore(release): publish new version
- @standardnotes/websockets-server@1.1.2
2022-10-13 10:11:43 +00:00
Karol Sójko
4e21edce6b fix(websockets): add http client binding 2022-10-13 12:09:20 +02:00
standardci
5663841145 chore(release): publish new version
- @standardnotes/websockets-server@1.1.1
2022-10-13 09:59:16 +00:00
Karol Sójko
2f7ef497ab fix(websockets): remove unnecessary sns bindings 2022-10-13 11:57:09 +02:00
61 changed files with 340 additions and 484 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

@@ -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.31.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.31.1...@standardnotes/api-gateway@1.31.2) (2022-10-13)
### Bug Fixes
* **api-gateway:** make web sockets url optional ([219b1ba](https://github.com/standardnotes/api-gateway/commit/219b1baa41f24ba140f24f48bf9c9d7e01288ed5))
## [1.31.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.31.0...@standardnotes/api-gateway@1.31.1) (2022-10-13)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.31.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.30.1...@standardnotes/api-gateway@1.31.0) (2022-10-13)
### Features
* **auth:** remove websocket handling in favor of websockets service ([86ae4a5](https://github.com/standardnotes/api-gateway/commit/86ae4a59a3ac7915ad96ed5176b545f4d005e837))
## [1.30.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.30.0...@standardnotes/api-gateway@1.30.1) (2022-10-13)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.30.1",
"version": "1.31.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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', true))
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,21 @@ 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> {
if (!this.webSocketServerUrl) {
this.logger.debug('Websockets Server URL not defined. Skipped request to WebSockets API.')
return
}
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

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.45.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.44.0...@standardnotes/auth-server@1.45.0) (2022-10-13)
### Features
* publish workspace invite accepted event for websockets ([86379eb](https://github.com/standardnotes/server/commit/86379eb96d7231d6a76ee91350accef2d44a941d))
# [1.44.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.43.1...@standardnotes/auth-server@1.44.0) (2022-10-13)
### Features
* **auth:** remove websocket handling in favor of websockets service ([86ae4a5](https://github.com/standardnotes/server/commit/86ae4a59a3ac7915ad96ed5176b545f4d005e837))
## [1.43.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.43.0...@standardnotes/auth-server@1.43.1) (2022-10-13)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.43.1",
"version": "1.45.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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,25 @@
import 'reflect-metadata'
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
import {
DomainEventPublisherInterface,
UserRolesChangedEvent,
WebSocketMessageRequestedEvent,
} 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 +32,22 @@ 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)
domainEventFactory.createWebSocketMessageRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<WebSocketMessageRequestedEvent>)
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),
}),
)
}
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.27](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.26...@standardnotes/domain-events-infra@1.8.27) (2022-10-13)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.8.26](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.25...@standardnotes/domain-events-infra@1.8.26) (2022-10-13)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.8.26",
"version": "1.8.27",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.68.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.67.0...@standardnotes/domain-events@2.68.0) (2022-10-13)
### Features
* publish workspace invite accepted event for websockets ([86379eb](https://github.com/standardnotes/server/commit/86379eb96d7231d6a76ee91350accef2d44a941d))
# [2.67.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.66.3...@standardnotes/domain-events@2.67.0) (2022-10-13)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.67.0",
"version": "2.68.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { WorkspaceInviteAcceptedEventPayload } from './WorkspaceInviteAcceptedEventPayload'
export interface WorkspaceInviteAcceptedEvent extends DomainEventInterface {
type: 'WORKSPACE_INVITE_ACCEPTED'
payload: WorkspaceInviteAcceptedEventPayload
}

View File

@@ -0,0 +1,5 @@
export interface WorkspaceInviteAcceptedEventPayload {
inviterUuid: string
inviteeUuid: string
workspaceUuid: string
}

View File

@@ -102,6 +102,8 @@ export * from './Event/UserSignedInEvent'
export * from './Event/UserSignedInEventPayload'
export * from './Event/WebSocketMessageRequestedEvent'
export * from './Event/WebSocketMessageRequestedEventPayload'
export * from './Event/WorkspaceInviteAcceptedEvent'
export * from './Event/WorkspaceInviteAcceptedEventPayload'
export * from './Event/WorkspaceInviteCreatedEvent'
export * from './Event/WorkspaceInviteCreatedEventPayload'

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.6](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.4.5...@standardnotes/event-store@1.4.6) (2022-10-13)
**Note:** Version bump only for package @standardnotes/event-store
## [1.4.5](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.4.4...@standardnotes/event-store@1.4.5) (2022-10-13)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.4.5",
"version": "1.4.6",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.18](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.17...@standardnotes/files-server@1.6.18) (2022-10-13)
**Note:** Version bump only for package @standardnotes/files-server
## [1.6.17](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.16...@standardnotes/files-server@1.6.17) (2022-10-13)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.6.17",
"version": "1.6.18",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.46](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.45...@standardnotes/scheduler-server@1.10.46) (2022-10-13)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.45](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.44...@standardnotes/scheduler-server@1.10.45) (2022-10-13)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.10.45",
"version": "1.10.46",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.9.7...@standardnotes/syncing-server@1.9.8) (2022-10-13)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.9.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.9.6...@standardnotes/syncing-server@1.9.7) (2022-10-13)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.9.7",
"version": "1.9.8",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.1.2...@standardnotes/websockets-server@1.1.3) (2022-10-13)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.1.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.1.1...@standardnotes/websockets-server@1.1.2) (2022-10-13)
### Bug Fixes
* **websockets:** add http client binding ([4e21edc](https://github.com/standardnotes/server/commit/4e21edce6b034312f121db4dce716e82ff7d5eaa))
## [1.1.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.1.0...@standardnotes/websockets-server@1.1.1) (2022-10-13)
### Bug Fixes
* **websockets:** remove unnecessary sns bindings ([2f7ef49](https://github.com/standardnotes/server/commit/2f7ef497ab4875685d6a0f282eeae11005900bc3))
# 1.1.0 (2022-10-13)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.1.0",
"version": "1.1.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -1,4 +1,5 @@
import * as winston from 'winston'
import axios, { AxiosInstance } from 'axios'
import Redis from 'ioredis'
import * as AWS from 'aws-sdk'
import { Container } from 'inversify'
@@ -71,15 +72,6 @@ export class ContainerConfigLoader {
})
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
if (env.get('SNS_AWS_REGION', true)) {
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(
new AWS.SNS({
apiVersion: 'latest',
region: env.get('SNS_AWS_REGION', true),
}),
)
}
if (env.get('SQS_QUEUE_URL', true)) {
const sqsConfig: AWS.SQS.Types.ClientConfiguration = {
apiVersion: 'latest',
@@ -114,8 +106,6 @@ export class ContainerConfigLoader {
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL)
.toConstantValue(+env.get('WEB_SOCKET_CONNECTION_TOKEN_TTL', true))
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
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))
@@ -135,6 +125,7 @@ export class ContainerConfigLoader {
.to(WebSocketMessageRequestedEventHandler)
// Services
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
.toConstantValue(new TokenDecoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))

View File

@@ -1,7 +1,6 @@
const TYPES = {
Logger: Symbol.for('Logger'),
Redis: Symbol.for('Redis'),
SNS: Symbol.for('SNS'),
SQS: Symbol.for('SQS'),
// Controller
WebSocketsController: Symbol.for('WebSocketsController'),
@@ -14,8 +13,6 @@ const TYPES = {
WEB_SOCKET_CONNECTION_TOKEN_SECRET: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_SECRET'),
WEB_SOCKET_CONNECTION_TOKEN_TTL: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_TTL'),
REDIS_URL: Symbol.for('REDIS_URL'),
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.13.2...@standardnotes/workspace-server@1.14.0) (2022-10-13)
### Features
* publish workspace invite accepted event for websockets ([86379eb](https://github.com/standardnotes/server/commit/86379eb96d7231d6a76ee91350accef2d44a941d))
## [1.13.2](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.13.1...@standardnotes/workspace-server@1.13.2) (2022-10-13)
**Note:** Version bump only for package @standardnotes/workspace-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/workspace-server",
"version": "1.13.2",
"version": "1.14.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -15,6 +15,54 @@ 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: 'workspace',
},
payload: {
userUuid: '1-2-3',
message: 'foobar',
},
type: 'WEB_SOCKET_MESSAGE_REQUESTED',
})
})
it('should create a WORKSPACE_INVITE_ACCEPTED event', () => {
expect(
createFactory().createWorkspaceInviteAcceptedEvent({
inviterUuid: '1-2-3',
inviteeUuid: '2-3-4',
workspaceUuid: 'w-1-2-3',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'workspace',
},
payload: {
inviterUuid: '1-2-3',
inviteeUuid: '2-3-4',
workspaceUuid: 'w-1-2-3',
},
type: 'WORKSPACE_INVITE_ACCEPTED',
})
})
it('should create a WORKSPACE_INVITE_CREATED event', () => {
expect(
createFactory().createWorkspaceInviteCreatedEvent({

View File

@@ -1,4 +1,9 @@
import { DomainEventService, WorkspaceInviteCreatedEvent } from '@standardnotes/domain-events'
import {
DomainEventService,
WebSocketMessageRequestedEvent,
WorkspaceInviteAcceptedEvent,
WorkspaceInviteCreatedEvent,
} from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
@@ -10,6 +15,40 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createWorkspaceInviteAcceptedEvent(dto: {
inviterUuid: string
inviteeUuid: string
workspaceUuid: string
}): WorkspaceInviteAcceptedEvent {
return {
type: 'WORKSPACE_INVITE_ACCEPTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.inviteeUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Workspace,
},
payload: dto,
}
}
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: string }): WebSocketMessageRequestedEvent {
return {
type: 'WEB_SOCKET_MESSAGE_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.userUuid,
userIdentifierType: 'uuid',
},
origin: DomainEventService.Workspace,
},
payload: dto,
}
}
createWorkspaceInviteCreatedEvent(dto: {
inviterUuid: string
inviteeEmail: string

View File

@@ -1,4 +1,9 @@
import { WorkspaceInviteCreatedEvent } from '@standardnotes/domain-events'
import { JSONString } from '@standardnotes/common'
import {
WebSocketMessageRequestedEvent,
WorkspaceInviteAcceptedEvent,
WorkspaceInviteCreatedEvent,
} from '@standardnotes/domain-events'
export interface DomainEventFactoryInterface {
createWorkspaceInviteCreatedEvent(dto: {
@@ -7,4 +12,10 @@ export interface DomainEventFactoryInterface {
inviteUuid: string
workspaceUuid: string
}): WorkspaceInviteCreatedEvent
createWorkspaceInviteAcceptedEvent(dto: {
inviterUuid: string
inviteeUuid: string
workspaceUuid: string
}): WorkspaceInviteAcceptedEvent
createWebSocketMessageRequestedEvent(dto: { userUuid: string; message: JSONString }): WebSocketMessageRequestedEvent
}

View File

@@ -7,14 +7,29 @@ import { WorkspaceUserRepositoryInterface } from '../../Workspace/WorkspaceUserR
import { AcceptInvitation } from './AcceptInvitation'
import { WorkspaceAccessLevel } from '@standardnotes/common'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import {
DomainEventPublisherInterface,
WebSocketMessageRequestedEvent,
WorkspaceInviteAcceptedEvent,
} from '@standardnotes/domain-events'
describe('AcceptInvitation', () => {
let workspaceInviteRepository: WorkspaceInviteRepositoryInterface
let workspaceUserRepository: WorkspaceUserRepositoryInterface
let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let timer: TimerInterface
let invite: WorkspaceInvite
const createUseCase = () => new AcceptInvitation(workspaceInviteRepository, workspaceUserRepository, timer)
const createUseCase = () =>
new AcceptInvitation(
workspaceInviteRepository,
workspaceUserRepository,
domainEventFactory,
domainEventPublisher,
timer,
)
beforeEach(() => {
invite = {
@@ -32,6 +47,17 @@ describe('AcceptInvitation', () => {
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createWebSocketMessageRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<WebSocketMessageRequestedEvent>)
domainEventFactory.createWorkspaceInviteAcceptedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<WorkspaceInviteAcceptedEvent>)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
})
it('should accept an invite and assign user to workspace', async () => {

View File

@@ -11,12 +11,16 @@ import { UseCaseInterface } from '../UseCaseInterface'
import { AcceptInvitationDTO } from './AcceptInvitationDTO'
import { AcceptInvitationResponse } from './AcceptInvitationResponse'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
@injectable()
export class AcceptInvitation implements UseCaseInterface {
constructor(
@inject(TYPES.WorkspaceInviteRepository) private workspaceInviteRepository: WorkspaceInviteRepositoryInterface,
@inject(TYPES.WorkspaceUserRepository) private workspaceUserRepository: WorkspaceUserRepositoryInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
@@ -45,6 +49,19 @@ export class AcceptInvitation implements UseCaseInterface {
await this.workspaceUserRepository.save(workspaceUser)
const event = this.domainEventFactory.createWorkspaceInviteAcceptedEvent({
inviteeUuid: invite.acceptingUserUuid,
inviterUuid: invite.inviterUuid,
workspaceUuid: invite.workspaceUuid,
})
await this.domainEventPublisher.publish(
this.domainEventFactory.createWebSocketMessageRequestedEvent({
userUuid: invite.inviterUuid,
message: JSON.stringify(event),
}),
)
return {
success: true,
}