-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): Add Novu-managed Bridge endpoint per environment (#6451)
- Loading branch information
Showing
41 changed files
with
800 additions
and
265 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -309,6 +309,7 @@ | |
"mailin", | ||
"Mailjet", | ||
"mailparser", | ||
"Maily", | ||
"Maizzle", | ||
"mansagroup", | ||
"mantine", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 4 additions & 1 deletion
5
apps/api/src/app/bridge/dtos/validate-bridge-url-response.dto.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; | ||
|
||
export class ValidateBridgeUrlResponseDto { | ||
@ApiProperty() | ||
isValid: boolean; | ||
|
||
@ApiPropertyOptional() | ||
error?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 3 additions & 8 deletions
11
apps/api/src/app/bridge/usecases/get-bridge-status/get-bridge-status.command.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,5 @@ | ||
import { IsUrl } from 'class-validator'; | ||
import { BaseCommand } from '@novu/application-generic'; | ||
import { EnvironmentLevelCommand } from '@novu/application-generic'; | ||
|
||
export class GetBridgeStatusCommand extends BaseCommand { | ||
@IsUrl({ | ||
require_protocol: true, | ||
require_tld: false, | ||
}) | ||
bridgeUrl: string; | ||
export class GetBridgeStatusCommand extends EnvironmentLevelCommand { | ||
statelessBridgeUrl?: string; | ||
} |
35 changes: 18 additions & 17 deletions
35
apps/api/src/app/bridge/usecases/get-bridge-status/get-bridge-status.usecase.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,35 @@ | ||
import { BadRequestException, Logger, Injectable } from '@nestjs/common'; | ||
import axios from 'axios'; | ||
import { HealthCheck, GetActionEnum, HttpQueryKeysEnum } from '@novu/framework'; | ||
import { Logger, Injectable } from '@nestjs/common'; | ||
import { HealthCheck, GetActionEnum } from '@novu/framework'; | ||
import { ExecuteBridgeRequest, ExecuteBridgeRequestCommand, ExecuteBridgeRequestDto } from '@novu/application-generic'; | ||
import { WorkflowOriginEnum } from '@novu/shared'; | ||
import { GetBridgeStatusCommand } from './get-bridge-status.command'; | ||
|
||
const axiosInstance = axios.create(); | ||
|
||
export const LOG_CONTEXT = 'GetBridgeStatusUsecase'; | ||
|
||
@Injectable() | ||
export class GetBridgeStatus { | ||
constructor(private executeBridgeRequest: ExecuteBridgeRequest) {} | ||
|
||
async execute(command: GetBridgeStatusCommand): Promise<HealthCheck> { | ||
try { | ||
const bridgeActionUrl = new URL(command.bridgeUrl); | ||
bridgeActionUrl.searchParams.set(HttpQueryKeysEnum.ACTION, GetActionEnum.HEALTH_CHECK); | ||
|
||
const response = await axiosInstance.get<HealthCheck>(bridgeActionUrl.toString(), { | ||
headers: { | ||
'Bypass-Tunnel-Reminder': 'true', | ||
'content-type': 'application/json', | ||
}, | ||
}); | ||
const response = (await this.executeBridgeRequest.execute( | ||
ExecuteBridgeRequestCommand.create({ | ||
environmentId: command.environmentId, | ||
action: GetActionEnum.HEALTH_CHECK, | ||
workflowOrigin: WorkflowOriginEnum.EXTERNAL, | ||
statelessBridgeUrl: command.statelessBridgeUrl, | ||
retriesLimit: 1, | ||
}) | ||
)) as ExecuteBridgeRequestDto<GetActionEnum.HEALTH_CHECK>; | ||
|
||
return response.data; | ||
return response; | ||
} catch (err: any) { | ||
Logger.error( | ||
`Failed to verify Bridge endpoint ${command.bridgeUrl} with error: ${(err as Error).message || err}`, | ||
`Failed to verify Bridge endpoint for environment ${command.environmentId} with error: ${(err as Error).message || err}`, | ||
(err as Error).stack, | ||
LOG_CONTEXT | ||
); | ||
throw new BadRequestException(`Bridge is not accessible. ${err.message}`); | ||
throw err; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 33 additions & 94 deletions
127
apps/api/src/app/bridge/usecases/preview-step/preview-step.usecase.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,114 +1,53 @@ | ||
import { createHmac } from 'crypto'; | ||
import axios from 'axios'; | ||
import { BadRequestException, Injectable } from '@nestjs/common'; | ||
import { PostActionEnum, HttpQueryKeysEnum } from '@novu/framework'; | ||
|
||
import { EnvironmentRepository } from '@novu/dal'; | ||
import { decryptApiKey } from '@novu/application-generic'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { PostActionEnum, HttpQueryKeysEnum, Event, JobStatusEnum, ExecuteOutput } from '@novu/framework'; | ||
import { ExecuteBridgeRequest, ExecuteBridgeRequestCommand } from '@novu/application-generic'; | ||
import { WorkflowOriginEnum } from '@novu/shared'; | ||
|
||
import { PreviewStepCommand } from './preview-step.command'; | ||
import { BridgeErrorCodeEnum } from '../../shared'; | ||
|
||
@Injectable() | ||
export class PreviewStep { | ||
constructor(private environmentRepository: EnvironmentRepository) {} | ||
|
||
async execute(command: PreviewStepCommand) { | ||
const environment = await this.environmentRepository.findOne({ _id: command.environmentId }); | ||
const bridgeUrl = command.bridgeUrl || environment?.echo.url; | ||
if (!bridgeUrl) { | ||
throw new BadRequestException('Bridge URL not found'); | ||
} | ||
|
||
const axiosInstance = axios.create(); | ||
try { | ||
const payload = this.mapPayload(command); | ||
const novuSignatureHeader = this.buildNovuSignature(environment, payload); | ||
const bridgeActionUrl = new URL(bridgeUrl); | ||
bridgeActionUrl.searchParams.set(HttpQueryKeysEnum.ACTION, PostActionEnum.PREVIEW); | ||
bridgeActionUrl.searchParams.set(HttpQueryKeysEnum.WORKFLOW_ID, command.workflowId); | ||
bridgeActionUrl.searchParams.set(HttpQueryKeysEnum.STEP_ID, command.stepId); | ||
|
||
const response = await axiosInstance.post(bridgeActionUrl.toString(), payload, { | ||
headers: { | ||
'content-type': 'application/json', | ||
'x-novu-signature': novuSignatureHeader, | ||
'novu-signature': novuSignatureHeader, | ||
constructor(private executeBridgeRequest: ExecuteBridgeRequest) {} | ||
|
||
async execute(command: PreviewStepCommand): Promise<ExecuteOutput> { | ||
const event = this.mapEvent(command); | ||
|
||
const response = (await this.executeBridgeRequest.execute( | ||
ExecuteBridgeRequestCommand.create({ | ||
environmentId: command.environmentId, | ||
action: PostActionEnum.PREVIEW, | ||
event, | ||
searchParams: { | ||
[HttpQueryKeysEnum.WORKFLOW_ID]: command.workflowId, | ||
[HttpQueryKeysEnum.STEP_ID]: command.stepId, | ||
}, | ||
}); | ||
|
||
if (!response.data?.outputs || !response.data?.metadata) { | ||
throw new BadRequestException({ | ||
code: BridgeErrorCodeEnum.BRIDGE_UNEXPECTED_RESPONSE, | ||
message: JSON.stringify(response.data), | ||
}); | ||
} | ||
|
||
return response.data; | ||
} catch (e: any) { | ||
if (e?.response?.status === 404) { | ||
throw new BadRequestException({ | ||
code: BridgeErrorCodeEnum.BRIDGE_ENDPOINT_NOT_FOUND, | ||
message: `Bridge Endpoint Was not found or not accessible. Endpoint: ${bridgeUrl}`, | ||
}); | ||
} | ||
|
||
if (e?.response?.status === 405) { | ||
throw new BadRequestException({ | ||
code: BridgeErrorCodeEnum.BRIDGE_ENDPOINT_NOT_FOUND, | ||
message: `Bridge Endpoint is not properly configured. : ${bridgeUrl}`, | ||
}); | ||
} | ||
|
||
if (e.code === BridgeErrorCodeEnum.BRIDGE_UNEXPECTED_RESPONSE) { | ||
throw e; | ||
} | ||
// TODO: pass the origin from the command | ||
workflowOrigin: WorkflowOriginEnum.EXTERNAL, | ||
retriesLimit: 1, | ||
}) | ||
)) as ExecuteOutput; | ||
|
||
// todo add status indication - check if e?.response?.status === 400 here | ||
if (e?.response?.data) { | ||
throw new BadRequestException(e.response.data); | ||
} | ||
|
||
throw new BadRequestException({ | ||
code: BridgeErrorCodeEnum.BRIDGE_UNEXPECTED_RESPONSE, | ||
message: `Un-expected Bridge response: ${e.message}`, | ||
}); | ||
} | ||
return response; | ||
} | ||
|
||
private mapPayload(command: PreviewStepCommand) { | ||
private mapEvent(command: PreviewStepCommand): Omit<Event, 'workflowId' | 'stepId' | 'action' | 'source'> { | ||
const payload = { | ||
inputs: command.controls || command.inputs || {}, | ||
controls: command.controls || command.inputs || {}, | ||
data: command.data || {}, | ||
/** @deprecated - use controls instead */ | ||
inputs: command.controls || {}, | ||
controls: command.controls || {}, | ||
/** @deprecated - use payload instead */ | ||
data: command.payload || {}, | ||
payload: command.payload || {}, | ||
state: [ | ||
{ | ||
stepId: 'trigger', | ||
outputs: command.data || {}, | ||
outputs: command.payload || {}, | ||
state: { status: JobStatusEnum.COMPLETED }, | ||
}, | ||
], | ||
subscriber: {}, | ||
}; | ||
|
||
return payload; | ||
} | ||
|
||
private buildNovuSignature( | ||
environment, | ||
payload: { data: any; inputs: any; controls: any; state: { outputs: any; stepId: string }[] } | ||
) { | ||
const timestamp = Date.now(); | ||
const xNovuSignature = `t=${timestamp},v1=${this.createHmacByApiKey( | ||
environment.apiKeys[0].key, | ||
timestamp, | ||
payload | ||
)}`; | ||
|
||
return xNovuSignature; | ||
} | ||
|
||
private createHmacByApiKey(secret: string, timestamp: number, payload) { | ||
const publicKey = `${timestamp}.${JSON.stringify(payload)}`; | ||
|
||
return createHmac('sha256', decryptApiKey(secret)).update(publicKey).digest('hex'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.