Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
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>
This commit is contained in:
380
marketing-agent/services/webhook-service/src/routes/webhooks.js
Normal file
380
marketing-agent/services/webhook-service/src/routes/webhooks.js
Normal file
@@ -0,0 +1,380 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user