Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
380 lines
12 KiB
JavaScript
380 lines
12 KiB
JavaScript
import express from 'express';
|
|
import Webhook from '../models/Webhook.js';
|
|
import WebhookLog from '../models/WebhookLog.js';
|
|
import { validateRequest } from '../middleware/validateRequest.js';
|
|
import { authenticate } from '../middleware/auth.js';
|
|
import Joi from 'joi';
|
|
|
|
const router = express.Router();
|
|
|
|
// Validation schemas
|
|
const createWebhookSchema = Joi.object({
|
|
name: Joi.string().required().min(1).max(100),
|
|
description: Joi.string().optional().max(500),
|
|
url: Joi.string().required().uri({ scheme: ['http', 'https'] }),
|
|
events: Joi.array().items(
|
|
Joi.string().valid(
|
|
'message.sent',
|
|
'message.delivered',
|
|
'message.failed',
|
|
'message.read',
|
|
'contact.created',
|
|
'contact.updated',
|
|
'contact.deleted',
|
|
'campaign.started',
|
|
'campaign.completed',
|
|
'campaign.failed',
|
|
'conversion.tracked',
|
|
'workflow.triggered',
|
|
'workflow.completed',
|
|
'account.updated'
|
|
)
|
|
).required().min(1),
|
|
headers: Joi.object().pattern(Joi.string(), Joi.string()).optional(),
|
|
retryConfig: Joi.object({
|
|
maxRetries: Joi.number().min(0).max(10).optional(),
|
|
retryDelay: Joi.number().min(1000).max(60000).optional()
|
|
}).optional(),
|
|
timeout: Joi.number().min(1000).max(60000).optional(),
|
|
metadata: Joi.object().optional()
|
|
});
|
|
|
|
const updateWebhookSchema = Joi.object({
|
|
name: Joi.string().min(1).max(100).optional(),
|
|
description: Joi.string().max(500).optional(),
|
|
url: Joi.string().uri({ scheme: ['http', 'https'] }).optional(),
|
|
events: Joi.array().items(
|
|
Joi.string().valid(
|
|
'message.sent',
|
|
'message.delivered',
|
|
'message.failed',
|
|
'message.read',
|
|
'contact.created',
|
|
'contact.updated',
|
|
'contact.deleted',
|
|
'campaign.started',
|
|
'campaign.completed',
|
|
'campaign.failed',
|
|
'conversion.tracked',
|
|
'workflow.triggered',
|
|
'workflow.completed',
|
|
'account.updated'
|
|
)
|
|
).min(1).optional(),
|
|
headers: Joi.object().pattern(Joi.string(), Joi.string()).optional(),
|
|
active: Joi.boolean().optional(),
|
|
retryConfig: Joi.object({
|
|
maxRetries: Joi.number().min(0).max(10).optional(),
|
|
retryDelay: Joi.number().min(1000).max(60000).optional()
|
|
}).optional(),
|
|
timeout: Joi.number().min(1000).max(60000).optional(),
|
|
metadata: Joi.object().optional()
|
|
});
|
|
|
|
const testWebhookSchema = Joi.object({
|
|
event: Joi.string().required(),
|
|
payload: Joi.object().required()
|
|
});
|
|
|
|
// Create webhook
|
|
router.post('/', authenticate, validateRequest(createWebhookSchema), async (req, res, next) => {
|
|
try {
|
|
const webhook = new Webhook({
|
|
accountId: req.user.accountId,
|
|
...req.body,
|
|
headers: req.body.headers ? new Map(Object.entries(req.body.headers)) : new Map()
|
|
});
|
|
|
|
await webhook.save();
|
|
|
|
res.status(201).json({
|
|
webhook: {
|
|
id: webhook._id,
|
|
name: webhook.name,
|
|
description: webhook.description,
|
|
url: webhook.url,
|
|
events: webhook.events,
|
|
active: webhook.active,
|
|
secret: webhook.secret,
|
|
headers: Object.fromEntries(webhook.headers),
|
|
retryConfig: webhook.retryConfig,
|
|
timeout: webhook.timeout,
|
|
metadata: webhook.metadata ? Object.fromEntries(webhook.metadata) : {},
|
|
createdAt: webhook.createdAt
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// List webhooks
|
|
router.get('/', authenticate, async (req, res, next) => {
|
|
try {
|
|
const { page = 1, limit = 20, active } = req.query;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const query = { accountId: req.user.accountId };
|
|
if (active !== undefined) {
|
|
query.active = active === 'true';
|
|
}
|
|
|
|
const [webhooks, total] = await Promise.all([
|
|
Webhook.find(query)
|
|
.sort({ createdAt: -1 })
|
|
.skip(skip)
|
|
.limit(parseInt(limit)),
|
|
Webhook.countDocuments(query)
|
|
]);
|
|
|
|
res.json({
|
|
webhooks: webhooks.map(webhook => ({
|
|
id: webhook._id,
|
|
name: webhook.name,
|
|
description: webhook.description,
|
|
url: webhook.url,
|
|
events: webhook.events,
|
|
active: webhook.active,
|
|
stats: webhook.stats,
|
|
lastError: webhook.lastError,
|
|
createdAt: webhook.createdAt,
|
|
updatedAt: webhook.updatedAt
|
|
})),
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
pages: Math.ceil(total / limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get webhook details
|
|
router.get('/:id', authenticate, async (req, res, next) => {
|
|
try {
|
|
const webhook = await Webhook.findOne({
|
|
_id: req.params.id,
|
|
accountId: req.user.accountId
|
|
});
|
|
|
|
if (!webhook) {
|
|
return res.status(404).json({ error: 'Webhook not found' });
|
|
}
|
|
|
|
res.json({
|
|
webhook: {
|
|
id: webhook._id,
|
|
name: webhook.name,
|
|
description: webhook.description,
|
|
url: webhook.url,
|
|
events: webhook.events,
|
|
active: webhook.active,
|
|
secret: webhook.secret,
|
|
headers: Object.fromEntries(webhook.headers),
|
|
retryConfig: webhook.retryConfig,
|
|
timeout: webhook.timeout,
|
|
metadata: webhook.metadata ? Object.fromEntries(webhook.metadata) : {},
|
|
stats: webhook.stats,
|
|
lastError: webhook.lastError,
|
|
createdAt: webhook.createdAt,
|
|
updatedAt: webhook.updatedAt
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Update webhook
|
|
router.put('/:id', authenticate, validateRequest(updateWebhookSchema), async (req, res, next) => {
|
|
try {
|
|
const webhook = await Webhook.findOne({
|
|
_id: req.params.id,
|
|
accountId: req.user.accountId
|
|
});
|
|
|
|
if (!webhook) {
|
|
return res.status(404).json({ error: 'Webhook not found' });
|
|
}
|
|
|
|
// Update fields
|
|
Object.keys(req.body).forEach(key => {
|
|
if (key === 'headers') {
|
|
webhook.headers = new Map(Object.entries(req.body.headers));
|
|
} else if (key === 'metadata') {
|
|
webhook.metadata = new Map(Object.entries(req.body.metadata));
|
|
} else {
|
|
webhook[key] = req.body[key];
|
|
}
|
|
});
|
|
|
|
await webhook.save();
|
|
|
|
res.json({
|
|
webhook: {
|
|
id: webhook._id,
|
|
name: webhook.name,
|
|
description: webhook.description,
|
|
url: webhook.url,
|
|
events: webhook.events,
|
|
active: webhook.active,
|
|
headers: Object.fromEntries(webhook.headers),
|
|
retryConfig: webhook.retryConfig,
|
|
timeout: webhook.timeout,
|
|
metadata: webhook.metadata ? Object.fromEntries(webhook.metadata) : {},
|
|
updatedAt: webhook.updatedAt
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Delete webhook
|
|
router.delete('/:id', authenticate, async (req, res, next) => {
|
|
try {
|
|
const webhook = await Webhook.findOneAndDelete({
|
|
_id: req.params.id,
|
|
accountId: req.user.accountId
|
|
});
|
|
|
|
if (!webhook) {
|
|
return res.status(404).json({ error: 'Webhook not found' });
|
|
}
|
|
|
|
// Also delete associated logs
|
|
await WebhookLog.deleteMany({ webhookId: webhook._id });
|
|
|
|
res.json({ message: 'Webhook deleted successfully' });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Test webhook
|
|
router.post('/:id/test', authenticate, validateRequest(testWebhookSchema), async (req, res, next) => {
|
|
try {
|
|
const webhook = await Webhook.findOne({
|
|
_id: req.params.id,
|
|
accountId: req.user.accountId
|
|
});
|
|
|
|
if (!webhook) {
|
|
return res.status(404).json({ error: 'Webhook not found' });
|
|
}
|
|
|
|
// Get webhook processor from app
|
|
const webhookProcessor = req.app.get('webhookProcessor');
|
|
const log = await webhookProcessor.callWebhook(
|
|
webhook,
|
|
req.body.event,
|
|
req.body.payload
|
|
);
|
|
|
|
res.json({
|
|
success: log.status === 'success',
|
|
log: {
|
|
id: log._id,
|
|
status: log.status,
|
|
request: {
|
|
url: log.request.url,
|
|
headers: Object.fromEntries(log.request.headers),
|
|
body: log.request.body
|
|
},
|
|
response: log.response ? {
|
|
status: log.response.status,
|
|
statusText: log.response.statusText,
|
|
headers: Object.fromEntries(log.response.headers),
|
|
body: log.response.body,
|
|
responseTime: log.response.responseTime
|
|
} : null,
|
|
error: log.error
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Get webhook logs
|
|
router.get('/:id/logs', authenticate, async (req, res, next) => {
|
|
try {
|
|
const { page = 1, limit = 20, status } = req.query;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const webhook = await Webhook.findOne({
|
|
_id: req.params.id,
|
|
accountId: req.user.accountId
|
|
});
|
|
|
|
if (!webhook) {
|
|
return res.status(404).json({ error: 'Webhook not found' });
|
|
}
|
|
|
|
const query = { webhookId: webhook._id };
|
|
if (status) {
|
|
query.status = status;
|
|
}
|
|
|
|
const [logs, total] = await Promise.all([
|
|
WebhookLog.find(query)
|
|
.sort({ createdAt: -1 })
|
|
.skip(skip)
|
|
.limit(parseInt(limit)),
|
|
WebhookLog.countDocuments(query)
|
|
]);
|
|
|
|
res.json({
|
|
logs: logs.map(log => ({
|
|
id: log._id,
|
|
event: log.event,
|
|
status: log.status,
|
|
attempts: log.attempts,
|
|
response: log.response ? {
|
|
status: log.response.status,
|
|
responseTime: log.response.responseTime
|
|
} : null,
|
|
error: log.error,
|
|
createdAt: log.createdAt,
|
|
completedAt: log.completedAt
|
|
})),
|
|
pagination: {
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
total,
|
|
pages: Math.ceil(total / limit)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// Regenerate webhook secret
|
|
router.post('/:id/regenerate-secret', authenticate, async (req, res, next) => {
|
|
try {
|
|
const webhook = await Webhook.findOne({
|
|
_id: req.params.id,
|
|
accountId: req.user.accountId
|
|
});
|
|
|
|
if (!webhook) {
|
|
return res.status(404).json({ error: 'Webhook not found' });
|
|
}
|
|
|
|
// Generate new secret
|
|
webhook.secret = CryptoJS.lib.WordArray.random(32).toString();
|
|
await webhook.save();
|
|
|
|
res.json({
|
|
secret: webhook.secret
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
export default router; |