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