Files
lislgosms/api/src/send-chain/gateway-events.controller.ts
T

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;
}
}
}