Files
Goa-gel-fullstack/backend/src/modules/webhooks/services/webhooks.service.ts

223 lines
6.5 KiB
TypeScript
Raw Normal View History

import { Injectable, Logger, NotFoundException, BadRequestException, Inject } from '@nestjs/common';
import { Webhook } from '../../../database/models/webhook.model';
import { WebhookLog, WebhookLogStatus } from '../../../database/models/webhook-log.model';
import { CreateWebhookDto, UpdateWebhookDto, WebhookTestResultDto } from '../dto';
import { PaginationDto } from '../../../common/dto/pagination.dto';
import * as crypto from 'crypto';
import { paginate, PaginatedResult } from '../../../common/utils/pagination.util';
@Injectable()
export class WebhooksService {
private readonly logger = new Logger(WebhooksService.name);
constructor(
@Inject(Webhook)
private webhookRepository: typeof Webhook,
@Inject(WebhookLog)
private webhookLogRepository: typeof WebhookLog,
) {}
async register(departmentId: string, dto: CreateWebhookDto): Promise<Webhook> {
try {
this.logger.debug(
`Registering webhook for department: ${departmentId}, URL: ${dto.url}`,
);
const secret = crypto.randomBytes(32).toString('hex');
const saved = await this.webhookRepository.query().insertAndFetch({
departmentId,
url: dto.url,
events: dto.events as any,
secretHash: secret,
isActive: true,
});
this.logger.log(`Webhook registered: ${saved.id}`);
return saved;
} catch (error) {
this.logger.error('Failed to register webhook', error);
throw error;
}
}
async findAll(departmentId: string): Promise<Webhook[]> {
try {
return await this.webhookRepository.query()
.where({ departmentId })
.orderBy('created_at', 'DESC');
} catch (error) {
this.logger.error(`Failed to find webhooks for department ${departmentId}`, error);
throw error;
}
}
async findById(id: string): Promise<Webhook> {
try {
const webhook = await this.webhookRepository.query().findById(id);
if (!webhook) {
throw new NotFoundException(`Webhook not found: ${id}`);
}
return webhook;
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error(`Failed to find webhook: ${id}`, error);
throw error;
}
}
async update(id: string, dto: UpdateWebhookDto): Promise<Webhook> {
try {
this.logger.debug(`Updating webhook: ${id}`);
const webhook = await this.findById(id);
const updated = await webhook.$query().patchAndFetch(dto as any);
this.logger.log(`Webhook updated: ${id}`);
return updated;
} catch (error) {
this.logger.error(`Failed to update webhook: ${id}`, error);
throw error;
}
}
async delete(id: string): Promise<void> {
try {
this.logger.debug(`Deleting webhook: ${id}`);
const deletedCount = await this.webhookRepository.query().deleteById(id);
if (deletedCount === 0) {
throw new NotFoundException(`Webhook not found: ${id}`);
}
this.logger.log(`Webhook deleted: ${id}`);
} catch (error) {
this.logger.error(`Failed to delete webhook: ${id}`, error);
throw error;
}
}
async test(id: string): Promise<WebhookTestResultDto> {
try {
this.logger.debug(`Testing webhook: ${id}`);
const webhook = await this.findById(id);
const testPayload = {
eventType: 'TEST_EVENT',
timestamp: new Date(),
data: { test: true },
};
const startTime = Date.now();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(webhook.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': this.generateSignature(testPayload, webhook.secretHash),
},
body: JSON.stringify(testPayload),
signal: controller.signal,
});
clearTimeout(timeoutId);
const responseTime = Date.now() - startTime;
const result: WebhookTestResultDto = {
success: response.ok,
statusCode: response.status,
statusMessage: response.statusText,
responseTime,
};
this.logger.log(`Webhook test successful: ${id}, status=${response.status}`);
return result;
} catch (fetchError) {
const responseTime = Date.now() - startTime;
const error = fetchError as Error;
this.logger.warn(`Webhook test failed: ${id}, error=${error.message}`);
return {
success: false,
statusCode: 0,
statusMessage: 'Error',
responseTime,
error: error.message,
};
}
} catch (error) {
this.logger.error(`Failed to test webhook: ${id}`, error);
throw error;
}
}
async getLogs(webhookId: string, pagination: PaginationDto): Promise<PaginatedResult<WebhookLog>> {
try {
await this.findById(webhookId);
const query = this.webhookLogRepository.query()
.where({ webhookId })
.orderBy('created_at', 'DESC');
return await paginate(query, pagination.page, pagination.limit);
} catch (error) {
this.logger.error(`Failed to get logs for webhook: ${webhookId}`, error);
throw error;
}
}
generateSignature(payload: object, secret: string): string {
const message = JSON.stringify(payload);
return crypto.createHmac('sha256', secret).update(message).digest('hex');
}
verifySignature(payload: object, signature: string, secret: string): boolean {
const expectedSignature = this.generateSignature(payload, secret);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
async logWebhookAttempt(
webhookId: string,
eventType: string,
payload: object,
responseStatus: number | null,
responseBody: string | null,
responseTime: number | null,
retryCount: number,
): Promise<WebhookLog> {
try {
const status =
responseStatus && responseStatus >= 200 && responseStatus < 300
? WebhookLogStatus.SUCCESS
: WebhookLogStatus.FAILED;
return await this.webhookLogRepository.query().insert({
webhookId,
eventType,
payload: payload as any,
responseStatus,
responseBody,
responseTime,
retryCount,
status: status as any,
});
} catch (error) {
this.logger.error(`Failed to log webhook attempt: ${webhookId}`, error);
throw error;
}
}
}