xpeditis2.0/apps/backend/src/application/services/webhook.service.ts
David 4b00ee2601
Some checks failed
CI/CD Pipeline - Xpeditis PreProd / Frontend - Docker Build & Push (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Deploy to PreProd Server (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Run Smoke Tests (push) Blocked by required conditions
CI/CD Pipeline - Xpeditis PreProd / Backend - Build & Test (push) Failing after 5m53s
CI/CD Pipeline - Xpeditis PreProd / Backend - Docker Build & Push (push) Has been skipped
CI/CD Pipeline - Xpeditis PreProd / Frontend - Build & Test (push) Has been cancelled
fix: replace relative domain imports with TypeScript path aliases
- Replace all ../../domain/ imports with @domain/ across 67 files
- Configure NestJS to use tsconfig.build.json with rootDir
- Add tsc-alias to resolve path aliases after build
- This fixes 'Cannot find module' TypeScript compilation errors

Fixed files:
- 30 files in application layer
- 37 files in infrastructure layer
2025-11-16 19:20:58 +01:00

275 lines
7.3 KiB
TypeScript

/**
* Webhook Service
*
* Handles webhook management and triggering
*/
import { Injectable, Logger, Inject } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto';
import { firstValueFrom } from 'rxjs';
import { Webhook, WebhookEvent, WebhookStatus } from '@domain/entities/webhook.entity';
import {
WebhookRepository,
WEBHOOK_REPOSITORY,
WebhookFilters,
} from '@domain/ports/out/webhook.repository';
export interface CreateWebhookInput {
organizationId: string;
url: string;
events: WebhookEvent[];
description?: string;
headers?: Record<string, string>;
}
export interface UpdateWebhookInput {
url?: string;
events?: WebhookEvent[];
description?: string;
headers?: Record<string, string>;
}
export interface WebhookPayload {
event: WebhookEvent;
timestamp: string;
data: any;
organizationId: string;
}
@Injectable()
export class WebhookService {
private readonly logger = new Logger(WebhookService.name);
private readonly MAX_RETRIES = 3;
private readonly RETRY_DELAY_MS = 5000;
constructor(
@Inject(WEBHOOK_REPOSITORY)
private readonly webhookRepository: WebhookRepository,
private readonly httpService: HttpService
) {}
/**
* Create a new webhook
*/
async createWebhook(input: CreateWebhookInput): Promise<Webhook> {
const secret = this.generateSecret();
const webhook = Webhook.create({
id: uuidv4(),
organizationId: input.organizationId,
url: input.url,
events: input.events,
secret,
description: input.description,
headers: input.headers,
});
await this.webhookRepository.save(webhook);
this.logger.log(`Webhook created: ${webhook.id} for organization ${input.organizationId}`);
return webhook;
}
/**
* Get webhook by ID
*/
async getWebhookById(id: string): Promise<Webhook | null> {
return this.webhookRepository.findById(id);
}
/**
* Get webhooks by organization
*/
async getWebhooksByOrganization(organizationId: string): Promise<Webhook[]> {
return this.webhookRepository.findByOrganization(organizationId);
}
/**
* Get webhooks with filters
*/
async getWebhooks(filters: WebhookFilters): Promise<Webhook[]> {
return this.webhookRepository.findByFilters(filters);
}
/**
* Update webhook
*/
async updateWebhook(id: string, updates: UpdateWebhookInput): Promise<Webhook> {
const webhook = await this.webhookRepository.findById(id);
if (!webhook) {
throw new Error('Webhook not found');
}
const updatedWebhook = webhook.update(updates);
await this.webhookRepository.save(updatedWebhook);
this.logger.log(`Webhook updated: ${id}`);
return updatedWebhook;
}
/**
* Activate webhook
*/
async activateWebhook(id: string): Promise<void> {
const webhook = await this.webhookRepository.findById(id);
if (!webhook) {
throw new Error('Webhook not found');
}
const activatedWebhook = webhook.activate();
await this.webhookRepository.save(activatedWebhook);
this.logger.log(`Webhook activated: ${id}`);
}
/**
* Deactivate webhook
*/
async deactivateWebhook(id: string): Promise<void> {
const webhook = await this.webhookRepository.findById(id);
if (!webhook) {
throw new Error('Webhook not found');
}
const deactivatedWebhook = webhook.deactivate();
await this.webhookRepository.save(deactivatedWebhook);
this.logger.log(`Webhook deactivated: ${id}`);
}
/**
* Delete webhook
*/
async deleteWebhook(id: string): Promise<void> {
await this.webhookRepository.delete(id);
this.logger.log(`Webhook deleted: ${id}`);
}
/**
* Trigger webhooks for an event
*/
async triggerWebhooks(event: WebhookEvent, organizationId: string, data: any): Promise<void> {
try {
const webhooks = await this.webhookRepository.findActiveByEvent(event, organizationId);
if (webhooks.length === 0) {
this.logger.debug(`No active webhooks found for event: ${event}`);
return;
}
const payload: WebhookPayload = {
event,
timestamp: new Date().toISOString(),
data,
organizationId,
};
// Trigger all webhooks in parallel
await Promise.allSettled(webhooks.map(webhook => this.triggerWebhook(webhook, payload)));
this.logger.log(`Triggered ${webhooks.length} webhooks for event: ${event}`);
} catch (error: any) {
this.logger.error(
`Error triggering webhooks: ${error?.message || 'Unknown error'}`,
error?.stack
);
}
}
/**
* Trigger a single webhook with retries
*/
private async triggerWebhook(webhook: Webhook, payload: WebhookPayload): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < this.MAX_RETRIES; attempt++) {
try {
if (attempt > 0) {
await this.delay(this.RETRY_DELAY_MS * attempt);
}
// Generate signature
const signature = this.generateSignature(payload, webhook.secret);
// Prepare headers
const headers = {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
'X-Webhook-Event': payload.event,
'X-Webhook-Timestamp': payload.timestamp,
...webhook.headers,
};
// Send HTTP request
const response = await firstValueFrom(
this.httpService.post(webhook.url, payload, {
headers,
timeout: 10000, // 10 seconds
})
);
if (response && response.status >= 200 && response.status < 300) {
// Success - record trigger
const updatedWebhook = webhook.recordTrigger();
await this.webhookRepository.save(updatedWebhook);
this.logger.log(`Webhook triggered successfully: ${webhook.id} (attempt ${attempt + 1})`);
return;
}
lastError = new Error(
`HTTP ${response?.status || 'Unknown'}: ${response?.statusText || 'Unknown error'}`
);
} catch (error: any) {
lastError = error;
this.logger.warn(
`Webhook trigger attempt ${attempt + 1} failed: ${webhook.id} - ${error?.message}`
);
}
}
// All retries failed - mark webhook as failed
const failedWebhook = webhook.markAsFailed();
await this.webhookRepository.save(failedWebhook);
this.logger.error(
`Webhook failed after ${this.MAX_RETRIES} attempts: ${webhook.id} - ${lastError?.message}`
);
}
/**
* Generate webhook secret
*/
private generateSecret(): string {
return crypto.randomBytes(32).toString('hex');
}
/**
* Generate HMAC signature for webhook payload
*/
private generateSignature(payload: any, secret: string): string {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex');
}
/**
* Verify webhook signature
*/
verifySignature(payload: any, signature: string, secret: string): boolean {
const expectedSignature = this.generateSignature(payload, secret);
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
/**
* Delay helper for retries
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}