199 lines
7.8 KiB
TypeScript
199 lines
7.8 KiB
TypeScript
import { BadRequestException, Body, Controller, Optional, Post } from '@nestjs/common';
|
|
import { ApiTags } from '@nestjs/swagger';
|
|
import {
|
|
GatewayInboundAuthDto,
|
|
GatewayInboundSubmitDto,
|
|
GatewayPendingDeliveryQueryDto,
|
|
GatewayDownstreamAcknowledgedDto,
|
|
GatewayDownstreamFailureType,
|
|
GatewayDownstreamSentDto,
|
|
GatewayDownstreamRecoveryStatusDto,
|
|
GatewayReceiptEventDto,
|
|
GatewaySubmitDeadLetterDto,
|
|
GatewaySubmitResultDto,
|
|
GatewaySubmitSegmentResultDto,
|
|
GatewayUplinkEventDto,
|
|
} from './send-chain.contracts';
|
|
import { SendChainService } from './send-chain.service';
|
|
import { GatewayDownstreamConnectionEventDto } from '../sms-config/sms-config.contracts';
|
|
import { SmsConfigService } from '../sms-config/sms-config.service';
|
|
import { ProtocolLogsService, type ProtocolLogInput } from '../protocol-logs/protocol-logs.service';
|
|
import { SecurityDetectionService } from '../security-detection/security-detection.service';
|
|
import { MetricsService } from '../metrics/metrics.service';
|
|
|
|
@ApiTags('gateway-events')
|
|
@Controller('gateway/events')
|
|
export class GatewayEventsController {
|
|
constructor(
|
|
private readonly sendChain: SendChainService,
|
|
private readonly smsConfig: SmsConfigService,
|
|
private readonly protocolLogs: ProtocolLogsService,
|
|
private readonly security: SecurityDetectionService,
|
|
@Optional() private readonly metrics?: MetricsService,
|
|
) {}
|
|
|
|
@Post('submit-result')
|
|
submitResult(@Body() body: GatewaySubmitResultDto) {
|
|
return this.sendChain.handleSubmitResult(body);
|
|
}
|
|
|
|
@Post('submit-segment-result')
|
|
submitSegmentResult(@Body() body: GatewaySubmitSegmentResultDto) {
|
|
return this.sendChain.handleSubmitSegmentResult(body);
|
|
}
|
|
|
|
@Post('receipt/intake')
|
|
receiptIntake(@Body() body: GatewayReceiptEventDto) {
|
|
return this.trackGatewayEvent('deliver_receipt', body, () => this.sendChain.intakeReceipt(body));
|
|
}
|
|
|
|
@Post('receipt')
|
|
receipt(@Body() body: GatewayReceiptEventDto) {
|
|
return this.trackGatewayEvent('deliver_receipt', body, () => this.sendChain.handleReceipt(body));
|
|
}
|
|
|
|
@Post('uplink')
|
|
uplink(@Body() body: GatewayUplinkEventDto) {
|
|
return this.trackGatewayEvent('deliver_uplink', body, () => this.sendChain.handleUplink(body));
|
|
}
|
|
|
|
@Post('protocol-log')
|
|
protocolLog(@Body() body: ProtocolLogInput) {
|
|
const allowedPacket = (
|
|
body.direction === 'platform_to_channel'
|
|
&& ['submit', 'deliver_resp'].includes(body.eventType)
|
|
) || (
|
|
body.direction === 'channel_to_platform'
|
|
&& body.eventType === 'submit_resp'
|
|
) || (
|
|
body.direction === 'platform_to_client'
|
|
&& ['submit_resp', 'deliver_receipt', 'deliver_uplink'].includes(body.eventType)
|
|
) || (
|
|
body.direction === 'client_to_platform'
|
|
&& body.eventType === 'deliver_resp'
|
|
);
|
|
if (body.protocol !== 'cmpp' || !allowedPacket || !['success', 'failed'].includes(body.status)) {
|
|
throw new BadRequestException('Unsupported Gateway protocol log event');
|
|
}
|
|
this.protocolLogs.record(body);
|
|
return { accepted: true };
|
|
}
|
|
|
|
@Post('dead-letter')
|
|
deadLetter(@Body() body: GatewaySubmitDeadLetterDto) {
|
|
return this.sendChain.recordGatewaySubmitDeadLetter(body);
|
|
}
|
|
|
|
@Post('inbound/authenticate')
|
|
async authenticateInbound(@Body() body: GatewayInboundAuthDto) {
|
|
try {
|
|
return await this.trackGatewayEvent('connect', body, () => this.sendChain.authenticateInboundApplication(body), 'client_to_platform');
|
|
} catch (error) {
|
|
if (body.remoteIp) await this.security.recordEvent({ ruleCode: 'cmpp_auth_failure', sourceIp: body.remoteIp, account: body.account, protocol: body.version ?? 'cmpp', resultCode: error instanceof Error ? error.name : 'AUTH_FAILED' }).catch(() => undefined);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
@Post('inbound/submit')
|
|
submitInbound(@Body() body: GatewayInboundSubmitDto) {
|
|
return this.trackGatewayEvent('submit', body, () => this.sendChain.submitInboundMessage(body), 'client_to_platform');
|
|
}
|
|
|
|
@Post('inbound/connection')
|
|
inboundConnection(@Body() body: GatewayDownstreamConnectionEventDto) {
|
|
return this.smsConfig.recordDownstreamConnectionEvent(body);
|
|
}
|
|
|
|
@Post('downstream/pending')
|
|
pendingDownstream(@Body() body: GatewayPendingDeliveryQueryDto) {
|
|
return this.sendChain.listPendingDownstreamDeliveries(body);
|
|
}
|
|
|
|
@Post('downstream/delivered')
|
|
downstreamDelivered(@Body() body: { id: string }) {
|
|
return this.sendChain.markDownstreamDeliveryDelivered(body.id);
|
|
}
|
|
|
|
@Post('downstream/sent')
|
|
downstreamSent(@Body() body: GatewayDownstreamSentDto) {
|
|
return this.sendChain.markDownstreamDeliverySent(body);
|
|
}
|
|
|
|
@Post('downstream/acknowledged')
|
|
downstreamAcknowledged(@Body() body: GatewayDownstreamAcknowledgedDto) {
|
|
return this.sendChain.acknowledgeDownstreamDelivery(body);
|
|
}
|
|
|
|
@Post('downstream/failed')
|
|
downstreamFailed(@Body() body: GatewayDownstreamSentDto & { errorMessage?: string; failureType?: GatewayDownstreamFailureType }) {
|
|
return this.sendChain.markDownstreamDeliveryFailed(body.id, body.errorMessage, body.failureType, body);
|
|
}
|
|
|
|
@Post('downstream/recovery-status')
|
|
downstreamRecoveryStatus(@Body() body: GatewayDownstreamRecoveryStatusDto) {
|
|
return this.sendChain.recordGatewayDownstreamRecoveryStatus(body);
|
|
}
|
|
|
|
private async trackGatewayEvent<T>(
|
|
eventType: string,
|
|
body: object,
|
|
action: () => Promise<T> | T,
|
|
direction: ProtocolLogInput['direction'] = 'channel_to_platform',
|
|
) {
|
|
const startedAt = Date.now();
|
|
const inboundMetricStartedAt = eventType === 'submit' && direction === 'client_to_platform'
|
|
? this.metrics?.beginCmppInboundStage()
|
|
: undefined;
|
|
const value = body as Record<string, unknown>;
|
|
const common: Omit<ProtocolLogInput, 'status'> = {
|
|
protocol: 'cmpp',
|
|
direction,
|
|
eventType,
|
|
tenantId: value.tenantId as string,
|
|
applicationId: value.applicationId as string,
|
|
channelId: value.channelId as string,
|
|
account: (value.account ?? value.loginAccount) as string,
|
|
messageId: (value.messageId ?? value.platformMessageId) as string,
|
|
gatewayMessageId: (value.gatewayMessageId ?? value.msgId ?? value.upstreamMessageId) as string,
|
|
phone: (value.phoneNumber ?? value.srcTerminalId ?? value.destinationId) as string,
|
|
resultCode: (value.result ?? value.submitStatus ?? value.rawStatus ?? value.status ?? value.stat) as string,
|
|
detail: { sequenceId: value.sequenceId, connectionId: value.connectionId },
|
|
};
|
|
try {
|
|
const result = await action();
|
|
const resultValue = result && typeof result === 'object'
|
|
? result as Record<string, unknown>
|
|
: {};
|
|
this.protocolLogs.record({
|
|
...common,
|
|
tenantId: (resultValue.tenantId ?? common.tenantId) as string,
|
|
applicationId: (resultValue.applicationId ?? common.applicationId) as string,
|
|
channelId: common.channelId ?? resultValue.channelId as string,
|
|
account: common.account ?? resultValue.account as string,
|
|
messageId: (resultValue.messageId ?? common.messageId) as string,
|
|
gatewayMessageId: common.gatewayMessageId
|
|
?? (resultValue.gatewayMessageId ?? resultValue.msgId) as string,
|
|
resultCode: common.resultCode
|
|
?? (resultValue.result ?? resultValue.status) as string,
|
|
status: 'success',
|
|
durationMs: Date.now() - startedAt,
|
|
});
|
|
if (inboundMetricStartedAt != null) {
|
|
this.metrics?.finishCmppInboundStage(inboundMetricStartedAt, 'total', 'success');
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
this.protocolLogs.record({
|
|
...common,
|
|
status: 'failed',
|
|
durationMs: Date.now() - startedAt,
|
|
detail: { error: error instanceof Error ? error.message : 'unknown error' },
|
|
});
|
|
if (inboundMetricStartedAt != null) {
|
|
this.metrics?.finishCmppInboundStage(inboundMetricStartedAt, 'total', 'error');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|