src/webhooks/webhook.service.ts
Methods |
|
constructor(prisma: PrismaService)
|
||||||
|
Defined in src/webhooks/webhook.service.ts:7
|
||||||
|
Parameters :
|
| Async createWebhook |
createWebhook(orgId: string, url: string, events: string[], secret?: string)
|
|
Defined in src/webhooks/webhook.service.ts:95
|
|
Create a new webhook subscription.
Returns :
unknown
|
| Async deleteWebhook |
deleteWebhook(id: string, orgId: string)
|
|
Defined in src/webhooks/webhook.service.ts:136
|
|
Delete a webhook.
Returns :
unknown
|
| Async fireEvent |
fireEvent(orgId: string, event: string, payload: any)
|
|
Defined in src/webhooks/webhook.service.ts:15
|
|
Fire an event to all matching webhooks for an organization. POSTs the payload with HMAC-SHA256 signature in X-Webhook-Signature header.
Returns :
Promise<void>
|
| Async listWebhooks | ||||||
listWebhooks(orgId: string)
|
||||||
|
Defined in src/webhooks/webhook.service.ts:117
|
||||||
|
List all webhooks for an organization.
Parameters :
Returns :
unknown
|
| Async testWebhook |
testWebhook(id: string, orgId: string)
|
|
Defined in src/webhooks/webhook.service.ts:151
|
|
Send a test ping to a webhook.
Returns :
unknown
|
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
import * as crypto from 'crypto';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class WebhookService {
private readonly logger = new Logger(WebhookService.name);
constructor(private prisma: PrismaService) {}
/**
* Fire an event to all matching webhooks for an organization.
* POSTs the payload with HMAC-SHA256 signature in X-Webhook-Signature header.
*/
async fireEvent(
orgId: string,
event: string,
payload: any,
): Promise<void> {
const webhooks = await this.prisma.webhook.findMany({
where: {
organizationId: orgId,
isActive: true,
events: { has: event },
},
});
for (const webhook of webhooks) {
this.deliverWebhook(webhook, event, payload).catch((err) => {
this.logger.error(
`Failed to deliver webhook ${webhook.id} for event ${event}: ${err.message}`,
);
});
}
}
private async deliverWebhook(
webhook: any,
event: string,
payload: any,
): Promise<void> {
const body = JSON.stringify({
event,
timestamp: new Date().toISOString(),
data: payload,
});
const signature = crypto
.createHmac('sha256', webhook.secret)
.update(body)
.digest('hex');
try {
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': event,
},
body,
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
await this.prisma.webhook.update({
where: { id: webhook.id },
data: { lastTriggeredAt: new Date(), failCount: 0 },
});
} catch (error) {
const newFailCount = webhook.failCount + 1;
const updateData: any = { failCount: { increment: 1 } };
// Disable after 10 consecutive failures
if (newFailCount >= 10) {
updateData.isActive = false;
this.logger.warn(
`Webhook ${webhook.id} disabled after ${newFailCount} consecutive failures`,
);
}
await this.prisma.webhook.update({
where: { id: webhook.id },
data: updateData,
});
}
}
/**
* Create a new webhook subscription.
*/
async createWebhook(
orgId: string,
url: string,
events: string[],
secret?: string,
) {
const webhookSecret =
secret || crypto.randomBytes(32).toString('hex');
return this.prisma.webhook.create({
data: {
organizationId: orgId,
url,
secret: webhookSecret,
events,
},
});
}
/**
* List all webhooks for an organization.
*/
async listWebhooks(orgId: string) {
return this.prisma.webhook.findMany({
where: { organizationId: orgId },
select: {
id: true,
url: true,
events: true,
isActive: true,
failCount: true,
lastTriggeredAt: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },
});
}
/**
* Delete a webhook.
*/
async deleteWebhook(id: string, orgId: string) {
const webhook = await this.prisma.webhook.findFirst({
where: { id, organizationId: orgId },
});
if (!webhook) {
throw new NotFoundException('Webhook not found');
}
return this.prisma.webhook.delete({ where: { id } });
}
/**
* Send a test ping to a webhook.
*/
async testWebhook(id: string, orgId: string) {
const webhook = await this.prisma.webhook.findFirst({
where: { id, organizationId: orgId },
});
if (!webhook) {
throw new NotFoundException('Webhook not found');
}
const body = JSON.stringify({
event: 'webhook.test',
timestamp: new Date().toISOString(),
data: { message: 'This is a test ping from FleetCommand TMS' },
});
const signature = crypto
.createHmac('sha256', webhook.secret)
.update(body)
.digest('hex');
try {
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': 'webhook.test',
},
body,
signal: AbortSignal.timeout(10000),
});
return {
success: response.ok,
statusCode: response.status,
statusText: response.statusText,
};
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}
}