- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
365 lines
15 KiB
JavaScript
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
|