Files
iistwin/server/services/webhook.service.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

187 lines
4.9 KiB
TypeScript

import crypto from 'crypto';
import { storage } from '../storage';
import { generateBotServiceToken } from '../utils/jwt';
import { sendWebhook } from '../utils/webhook';
import type { BotSubscription } from '@shared/schema';
import { decrypt } from '../crypto';
export interface WebhookEvent {
event: string;
timestamp: string;
organizationId: number;
data: any;
}
export class WebhookService {
async dispatchEvent(event: WebhookEvent): Promise<void> {
const { organizationId, event: eventType, data } = event;
try {
let subscriptions: BotSubscription[] = [];
if (data.formId) {
const formSubs = await storage.getBotSubscriptionsByForm(data.formId, organizationId);
subscriptions = [...subscriptions, ...formSubs.filter(s => (s.events as string[]).includes(eventType))];
}
if (data.statusId) {
const statusSubs = await storage.getBotSubscriptionsByStatus(data.statusId, organizationId);
subscriptions = [...subscriptions, ...statusSubs.filter(s => (s.events as string[]).includes(eventType))];
}
if (data.transitionId) {
const transitionSubs = await storage.getBotSubscriptionsByTransition(data.transitionId, organizationId);
subscriptions = [...subscriptions, ...transitionSubs.filter(s => (s.events as string[]).includes(eventType))];
}
const uniqueBotIds = Array.from(new Set(subscriptions.map(s => s.botId)));
for (const botId of uniqueBotIds) {
const bot = await storage.getBot(botId, organizationId);
if (!bot || !bot.webhookUrl || !bot.webhookEnabled || !bot.isActive) {
continue;
}
// Generate short-lived access token for the bot
const botAccessToken = generateBotServiceToken(bot.id, organizationId);
const payload = {
...event,
bot: {
id: bot.id,
name: bot.name,
accessToken: botAccessToken // Short-lived token (10 min) for API callbacks
}
};
let signature = '';
if (bot.webhookSecret) {
const secret = decrypt(bot.webhookSecret);
signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
}
await sendWebhook(
bot.webhookUrl,
payload,
{
'X-Webhook-Signature': signature,
'X-Bot-Id': bot.id.toString(),
'X-Event-Type': eventType,
},
{
taskId: data.taskId,
organizationId,
botId: bot.id,
eventType,
}
);
}
} catch (error) {
console.error('Webhook dispatch error:', error);
}
}
async dispatchStatusChange(
organizationId: number,
taskId: number,
formId: number,
fromStatusId: number,
toStatusId: number,
transitionId: number,
userId: number
): Promise<void> {
const task = await storage.getTask(taskId, organizationId);
const user = await storage.getUser(userId);
await this.dispatchEvent({
event: 'status_change',
timestamp: new Date().toISOString(),
organizationId,
data: {
taskId,
formId,
task,
fromStatusId,
toStatusId,
transitionId,
statusId: toStatusId,
user: user ? {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email
} : null
}
});
}
async dispatchComment(
organizationId: number,
taskId: number,
formId: number,
message: any,
authorId: number
): Promise<void> {
const task = await storage.getTask(taskId, organizationId);
const author = await storage.getUser(authorId);
await this.dispatchEvent({
event: 'comment',
timestamp: new Date().toISOString(),
organizationId,
data: {
taskId,
formId,
task,
message,
author: author ? {
id: author.id,
firstName: author.firstName,
lastName: author.lastName,
email: author.email
} : null
}
});
}
async dispatchFieldChange(
organizationId: number,
taskId: number,
formId: number,
fieldId: number,
fieldCode: string,
oldValue: any,
newValue: any,
userId: number
): Promise<void> {
const task = await storage.getTask(taskId, organizationId);
const user = await storage.getUser(userId);
await this.dispatchEvent({
event: 'field_change',
timestamp: new Date().toISOString(),
organizationId,
data: {
taskId,
formId,
task,
fieldId,
fieldCode,
oldValue,
newValue,
user: user ? {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email
} : null
}
});
}
}
export const webhookService = new WebhookService();