template-saas/apps/backend/dist/modules/webhooks/services/webhook.service.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

365 lines
15 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var WebhookService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebhookService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const bullmq_1 = require("@nestjs/bullmq");
const bullmq_2 = require("bullmq");
const crypto = __importStar(require("crypto"));
const entities_1 = require("../entities");
const dto_1 = require("../dto");
let WebhookService = WebhookService_1 = class WebhookService {
constructor(webhookRepo, deliveryRepo, webhookQueue) {
this.webhookRepo = webhookRepo;
this.deliveryRepo = deliveryRepo;
this.webhookQueue = webhookQueue;
this.logger = new common_1.Logger(WebhookService_1.name);
}
generateSecret() {
return `whsec_${crypto.randomBytes(32).toString('hex')}`;
}
signPayload(payload, secret) {
const timestamp = Date.now();
const body = JSON.stringify(payload);
const signature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
async create(tenantId, userId, dto) {
const invalidEvents = dto.events.filter((e) => !dto_1.WEBHOOK_EVENTS.includes(e));
if (invalidEvents.length > 0) {
throw new common_1.BadRequestException(`Invalid events: ${invalidEvents.join(', ')}`);
}
const webhook = this.webhookRepo.create({
tenantId,
name: dto.name,
description: dto.description,
url: dto.url,
events: dto.events,
headers: dto.headers || {},
secret: this.generateSecret(),
createdBy: userId,
});
const saved = await this.webhookRepo.save(webhook);
this.logger.log(`Webhook created: ${saved.id} for tenant ${tenantId}`);
return this.toResponse(saved, true);
}
async findAll(tenantId) {
const webhooks = await this.webhookRepo.find({
where: { tenantId },
order: { createdAt: 'DESC' },
});
return Promise.all(webhooks.map((w) => this.toResponse(w)));
}
async findOne(tenantId, webhookId) {
const webhook = await this.webhookRepo.findOne({
where: { id: webhookId, tenantId },
});
if (!webhook) {
throw new common_1.NotFoundException('Webhook not found');
}
return this.toResponse(webhook);
}
async update(tenantId, webhookId, dto) {
const webhook = await this.webhookRepo.findOne({
where: { id: webhookId, tenantId },
});
if (!webhook) {
throw new common_1.NotFoundException('Webhook not found');
}
if (dto.events) {
const invalidEvents = dto.events.filter((e) => !dto_1.WEBHOOK_EVENTS.includes(e));
if (invalidEvents.length > 0) {
throw new common_1.BadRequestException(`Invalid events: ${invalidEvents.join(', ')}`);
}
}
Object.assign(webhook, {
name: dto.name ?? webhook.name,
description: dto.description ?? webhook.description,
url: dto.url ?? webhook.url,
events: dto.events ?? webhook.events,
headers: dto.headers ?? webhook.headers,
isActive: dto.isActive ?? webhook.isActive,
});
const saved = await this.webhookRepo.save(webhook);
this.logger.log(`Webhook updated: ${saved.id}`);
return this.toResponse(saved);
}
async remove(tenantId, webhookId) {
const webhook = await this.webhookRepo.findOne({
where: { id: webhookId, tenantId },
});
if (!webhook) {
throw new common_1.NotFoundException('Webhook not found');
}
await this.webhookRepo.remove(webhook);
this.logger.log(`Webhook deleted: ${webhookId}`);
}
async regenerateSecret(tenantId, webhookId) {
const webhook = await this.webhookRepo.findOne({
where: { id: webhookId, tenantId },
});
if (!webhook) {
throw new common_1.NotFoundException('Webhook not found');
}
webhook.secret = this.generateSecret();
await this.webhookRepo.save(webhook);
return { secret: webhook.secret };
}
async testWebhook(tenantId, webhookId, dto) {
const webhook = await this.webhookRepo.findOne({
where: { id: webhookId, tenantId },
});
if (!webhook) {
throw new common_1.NotFoundException('Webhook not found');
}
const eventType = dto.eventType || 'test.ping';
const payload = dto.payload || {
type: 'test.ping',
timestamp: new Date().toISOString(),
data: { message: 'This is a test webhook delivery' },
};
const delivery = this.deliveryRepo.create({
webhookId: webhook.id,
tenantId,
eventType,
payload,
status: entities_1.DeliveryStatus.PENDING,
});
const saved = await this.deliveryRepo.save(delivery);
await this.webhookQueue.add('deliver', {
deliveryId: saved.id,
webhookId: webhook.id,
url: webhook.url,
secret: webhook.secret,
headers: webhook.headers,
eventType,
payload,
}, { priority: 1 });
this.logger.log(`Test webhook queued: ${saved.id}`);
return this.toDeliveryResponse(saved);
}
async getDeliveries(tenantId, webhookId, query) {
const webhook = await this.webhookRepo.findOne({
where: { id: webhookId, tenantId },
});
if (!webhook) {
throw new common_1.NotFoundException('Webhook not found');
}
const page = query.page || 1;
const limit = Math.min(query.limit || 20, 100);
const skip = (page - 1) * limit;
const qb = this.deliveryRepo
.createQueryBuilder('d')
.where('d.webhook_id = :webhookId', { webhookId })
.andWhere('d.tenant_id = :tenantId', { tenantId });
if (query.status) {
qb.andWhere('d.status = :status', { status: query.status });
}
if (query.eventType) {
qb.andWhere('d.event_type = :eventType', { eventType: query.eventType });
}
qb.orderBy('d.created_at', 'DESC').skip(skip).take(limit);
const [items, total] = await qb.getManyAndCount();
return {
items: items.map((d) => this.toDeliveryResponse(d)),
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async retryDelivery(tenantId, webhookId, deliveryId) {
const delivery = await this.deliveryRepo.findOne({
where: { id: deliveryId, webhookId, tenantId },
relations: ['webhook'],
});
if (!delivery) {
throw new common_1.NotFoundException('Delivery not found');
}
if (delivery.status !== entities_1.DeliveryStatus.FAILED) {
throw new common_1.BadRequestException('Only failed deliveries can be retried');
}
delivery.status = entities_1.DeliveryStatus.RETRYING;
delivery.attempt = 1;
delivery.nextRetryAt = new Date();
await this.deliveryRepo.save(delivery);
await this.webhookQueue.add('deliver', {
deliveryId: delivery.id,
webhookId: delivery.webhookId,
url: delivery.webhook.url,
secret: delivery.webhook.secret,
headers: delivery.webhook.headers,
eventType: delivery.eventType,
payload: delivery.payload,
});
this.logger.log(`Delivery retry queued: ${delivery.id}`);
return this.toDeliveryResponse(delivery);
}
async getStats(webhookId) {
const result = await this.deliveryRepo
.createQueryBuilder('d')
.select([
'COUNT(*)::int as "totalDeliveries"',
'COUNT(*) FILTER (WHERE d.status = :delivered)::int as "successfulDeliveries"',
'COUNT(*) FILTER (WHERE d.status = :failed)::int as "failedDeliveries"',
'COUNT(*) FILTER (WHERE d.status IN (:...pending))::int as "pendingDeliveries"',
'MAX(d.delivered_at) as "lastDeliveryAt"',
])
.where('d.webhook_id = :webhookId', { webhookId })
.setParameters({
delivered: entities_1.DeliveryStatus.DELIVERED,
failed: entities_1.DeliveryStatus.FAILED,
pending: [entities_1.DeliveryStatus.PENDING, entities_1.DeliveryStatus.RETRYING],
})
.getRawOne();
const total = result.successfulDeliveries + result.failedDeliveries;
const successRate = total > 0 ? Math.round((result.successfulDeliveries / total) * 100) : 0;
return {
...result,
successRate,
};
}
async dispatch(tenantId, eventType, data) {
const webhooks = await this.webhookRepo.find({
where: { tenantId, isActive: true },
});
const subscribedWebhooks = webhooks.filter((w) => w.events.includes(eventType));
if (subscribedWebhooks.length === 0) {
return;
}
const payload = {
type: eventType,
timestamp: new Date().toISOString(),
data,
};
for (const webhook of subscribedWebhooks) {
const delivery = this.deliveryRepo.create({
webhookId: webhook.id,
tenantId,
eventType,
payload,
status: entities_1.DeliveryStatus.PENDING,
});
const saved = await this.deliveryRepo.save(delivery);
await this.webhookQueue.add('deliver', {
deliveryId: saved.id,
webhookId: webhook.id,
url: webhook.url,
secret: webhook.secret,
headers: webhook.headers,
eventType,
payload,
});
}
this.logger.log(`Event ${eventType} dispatched to ${subscribedWebhooks.length} webhooks for tenant ${tenantId}`);
}
getAvailableEvents() {
return [
{ name: 'user.created', description: 'A new user was created' },
{ name: 'user.updated', description: 'A user was updated' },
{ name: 'user.deleted', description: 'A user was deleted' },
{ name: 'subscription.created', description: 'A new subscription was created' },
{ name: 'subscription.updated', description: 'A subscription was updated' },
{ name: 'subscription.cancelled', description: 'A subscription was cancelled' },
{ name: 'invoice.paid', description: 'An invoice was paid' },
{ name: 'invoice.failed', description: 'An invoice payment failed' },
{ name: 'file.uploaded', description: 'A file was uploaded' },
{ name: 'file.deleted', description: 'A file was deleted' },
{ name: 'tenant.updated', description: 'Tenant settings were updated' },
];
}
async toResponse(webhook, includeSecret = false) {
const stats = await this.getStats(webhook.id);
return {
id: webhook.id,
name: webhook.name,
description: webhook.description,
url: webhook.url,
events: webhook.events,
headers: webhook.headers,
isActive: webhook.isActive,
createdAt: webhook.createdAt,
updatedAt: webhook.updatedAt,
...(includeSecret && { secret: webhook.secret }),
stats,
};
}
toDeliveryResponse(delivery) {
return {
id: delivery.id,
webhookId: delivery.webhookId,
eventType: delivery.eventType,
payload: delivery.payload,
status: delivery.status,
responseStatus: delivery.responseStatus,
responseBody: delivery.responseBody,
attempt: delivery.attempt,
maxAttempts: delivery.maxAttempts,
nextRetryAt: delivery.nextRetryAt,
lastError: delivery.lastError,
createdAt: delivery.createdAt,
deliveredAt: delivery.deliveredAt,
};
}
};
exports.WebhookService = WebhookService;
exports.WebhookService = WebhookService = WebhookService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(entities_1.WebhookEntity)),
__param(1, (0, typeorm_1.InjectRepository)(entities_1.WebhookDeliveryEntity)),
__param(2, (0, bullmq_1.InjectQueue)('webhooks')),
__metadata("design:paramtypes", [typeorm_2.Repository,
typeorm_2.Repository,
bullmq_2.Queue])
], WebhookService);
//# sourceMappingURL=webhook.service.js.map