mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 11:50:27 +00:00
- Add JWT authentication for cabinet users - Add Telegram WebApp authentication - Add subscription management endpoints - Add balance and transactions endpoints - Add referral system endpoints - Add tickets support for cabinet - Add webhooks and websocket for real-time updates - Add email verification service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
35 KiB
35 KiB
Руководство по интеграции WebSocket и Webhooks в веб-админку
Содержание
- Обзор
- Настройка WebSocket подключения
- Интеграция WebSocket в дашборд
- Управление Webhooks через API
- UI компоненты для Webhooks
- Примеры реализации
- Обработка ошибок
- Тестирование
Обзор
Веб-админка может использовать два механизма для получения обновлений:
- WebSocket - для real-time обновлений в интерфейсе (новые пользователи, платежи, тикеты)
- Webhooks - для настройки внешних интеграций (отправка событий на внешние серверы)
Настройка WebSocket подключения
Шаг 1: Создать WebSocket менеджер
Создайте утилиту для управления WebSocket подключением:
// utils/websocket.ts
class WebSocketManager {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private listeners: Map<string, Set<Function>> = new Map();
private apiToken: string;
constructor(apiToken: string) {
this.apiToken = apiToken;
}
connect(url: string): void {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log('WebSocket already connected');
return;
}
const wsUrl = `${url}?token=${this.apiToken}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.emit('connected', {});
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.emit('error', { error });
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.emit('disconnected', {});
this.attemptReconnect(url);
};
// Ping для keepalive каждые 30 секунд
setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
}
private handleMessage(data: any): void {
if (data.type === 'pong') {
return; // Игнорируем pong
}
if (data.type === 'connection') {
this.emit('connection', data);
return;
}
// Эмитим событие по типу
this.emit(data.type, data.payload);
}
private attemptReconnect(url: string): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
setTimeout(() => {
console.log(`Reconnecting... (attempt ${this.reconnectAttempts})`);
this.connect(url);
}, delay);
}
on(event: string, callback: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
off(event: string, callback: Function): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.delete(callback);
}
}
private emit(event: string, data: any): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`Error in event listener for ${event}:`, error);
}
});
}
}
disconnect(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
}
export default WebSocketManager;
Шаг 2: Инициализация в приложении
// App.tsx или main.tsx
import { useEffect, useState } from 'react';
import WebSocketManager from './utils/websocket';
import { getApiToken } from './utils/auth';
function App() {
const [wsManager, setWsManager] = useState<WebSocketManager | null>(null);
useEffect(() => {
const token = getApiToken();
if (!token) {
console.warn('No API token found, WebSocket will not connect');
return;
}
const manager = new WebSocketManager(token);
const wsUrl = process.env.REACT_APP_WS_URL || 'ws://localhost:8080/ws';
manager.connect(wsUrl);
setWsManager(manager);
// Обработка событий
manager.on('user.created', (payload) => {
console.log('New user created:', payload);
// Обновить список пользователей
// Показать уведомление
});
manager.on('payment.completed', (payload) => {
console.log('Payment completed:', payload);
// Обновить статистику
// Обновить баланс пользователя
});
manager.on('ticket.created', (payload) => {
console.log('New ticket created:', payload);
// Обновить список тикетов
// Показать уведомление
});
manager.on('ticket.status_changed', (payload) => {
console.log('Ticket status changed:', payload);
// Обновить статус тикета в списке
});
manager.on('ticket.message_added', (payload) => {
console.log('New message in ticket:', payload);
// Обновить список сообщений в тикете
// Показать уведомление о новом сообщении
});
return () => {
manager.disconnect();
};
}, []);
return (
// Ваш компонент приложения
);
}
Интеграция WebSocket в дашборд
Шаг 1: Создать React Hook для WebSocket
// hooks/useWebSocket.ts
import { useEffect, useState, useCallback } from 'react';
import { useWebSocketContext } from '../contexts/WebSocketContext';
export function useWebSocketEvent<T = any>(eventType: string) {
const { wsManager } = useWebSocketContext();
const [data, setData] = useState<T | null>(null);
useEffect(() => {
if (!wsManager) return;
const handler = (payload: T) => {
setData(payload);
};
wsManager.on(eventType, handler);
return () => {
wsManager.off(eventType, handler);
};
}, [wsManager, eventType]);
return data;
}
// Использование в компоненте
function Dashboard() {
const newUser = useWebSocketEvent('user.created');
const newPayment = useWebSocketEvent('payment.completed');
const newTicket = useWebSocketEvent('ticket.created');
useEffect(() => {
if (newUser) {
// Обновить счетчик пользователей
// Показать toast уведомление
}
}, [newUser]);
return (
// Ваш дашборд
);
}
Шаг 2: Обновление счетчиков в реальном времени
// components/DashboardStats.tsx
import { useState, useEffect } from 'react';
import { useWebSocketContext } from '../contexts/WebSocketContext';
import { fetchStats } from '../api/stats';
function DashboardStats() {
const { wsManager } = useWebSocketContext();
const [stats, setStats] = useState({
totalUsers: 0,
activeSubscriptions: 0,
openTickets: 0,
todayRevenue: 0,
});
// Загрузка начальных данных
useEffect(() => {
loadStats();
}, []);
// Подписка на события для обновления
useEffect(() => {
if (!wsManager) return;
const updateOnNewUser = () => {
setStats(prev => ({ ...prev, totalUsers: prev.totalUsers + 1 }));
};
const updateOnPayment = (payload: any) => {
setStats(prev => ({
...prev,
todayRevenue: prev.todayRevenue + (payload.amount_rubles || 0),
}));
};
const updateOnTicket = () => {
setStats(prev => ({ ...prev, openTickets: prev.openTickets + 1 }));
};
wsManager.on('user.created', updateOnNewUser);
wsManager.on('payment.completed', updateOnPayment);
wsManager.on('ticket.created', updateOnTicket);
wsManager.on('ticket.message_added', updateOnTicketMessage);
return () => {
wsManager.off('user.created', updateOnNewUser);
wsManager.off('payment.completed', updateOnPayment);
wsManager.off('ticket.created', updateOnTicket);
wsManager.off('ticket.message_added', updateOnTicketMessage);
};
}, [wsManager]);
const loadStats = async () => {
try {
const data = await fetchStats();
setStats(data);
} catch (error) {
console.error('Failed to load stats:', error);
}
};
return (
<div className="stats-grid">
<StatCard title="Всего пользователей" value={stats.totalUsers} />
<StatCard title="Активные подписки" value={stats.activeSubscriptions} />
<StatCard title="Открытые тикеты" value={stats.openTickets} />
<StatCard title="Доход сегодня" value={`${stats.todayRevenue} ₽`} />
</div>
);
}
Шаг 3: Уведомления о новых событиях
// components/NotificationCenter.tsx
import { useState, useEffect } from 'react';
import { useWebSocketContext } from '../contexts/WebSocketContext';
import { toast } from 'react-toastify';
interface Notification {
id: string;
type: string;
message: string;
timestamp: Date;
}
function NotificationCenter() {
const { wsManager } = useWebSocketContext();
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
if (!wsManager) return;
const handleNewUser = (payload: any) => {
const notification: Notification = {
id: `user-${payload.user_id}`,
type: 'user.created',
message: `Новый пользователь: @${payload.username || payload.telegram_id}`,
timestamp: new Date(),
};
addNotification(notification);
toast.info(notification.message);
};
const handleNewPayment = (payload: any) => {
const notification: Notification = {
id: `payment-${payload.transaction_id}`,
type: 'payment.completed',
message: `Пополнение баланса: ${payload.amount_rubles} ₽`,
timestamp: new Date(),
};
addNotification(notification);
toast.success(notification.message);
};
const handleNewTicket = (payload: any) => {
const notification: Notification = {
id: `ticket-${payload.ticket_id}`,
type: 'ticket.created',
message: `Новый тикет: ${payload.title}`,
timestamp: new Date(),
};
addNotification(notification);
toast.warning(notification.message, {
onClick: () => {
// Перейти к тикету
window.location.href = `/tickets/${payload.ticket_id}`;
},
});
};
const handleNewMessage = (payload: any) => {
const notification: Notification = {
id: `ticket-message-${payload.message_id}`,
type: 'ticket.message_added',
message: payload.is_from_admin
? `Новый ответ в тикете #${payload.ticket_id}`
: `Новое сообщение от пользователя в тикете #${payload.ticket_id}`,
timestamp: new Date(),
};
addNotification(notification);
toast.info(notification.message, {
onClick: () => {
// Перейти к тикету
window.location.href = `/tickets/${payload.ticket_id}`;
},
});
};
wsManager.on('user.created', handleNewUser);
wsManager.on('payment.completed', handleNewPayment);
wsManager.on('ticket.created', handleNewTicket);
wsManager.on('ticket.message_added', handleNewMessage);
return () => {
wsManager.off('user.created', handleNewUser);
wsManager.off('payment.completed', handleNewPayment);
wsManager.off('ticket.created', handleNewTicket);
wsManager.off('ticket.message_added', handleNewMessage);
};
}, [wsManager]);
const addNotification = (notification: Notification) => {
setNotifications(prev => [notification, ...prev].slice(0, 50)); // Храним последние 50
};
return (
<div className="notification-center">
{notifications.map(notif => (
<NotificationItem key={notif.id} notification={notif} />
))}
</div>
);
}
Управление Webhooks через API
Шаг 1: API клиент для webhooks
// api/webhooks.ts
import { apiClient } from './client';
export interface Webhook {
id: number;
name: string;
url: string;
event_type: string;
is_active: boolean;
description?: string;
created_at: string;
updated_at: string;
last_triggered_at?: string;
failure_count: number;
success_count: number;
}
export interface WebhookCreateRequest {
name: string;
url: string;
event_type: string;
secret?: string;
description?: string;
}
export interface WebhookUpdateRequest {
name?: string;
url?: string;
secret?: string;
description?: string;
is_active?: boolean;
}
export const webhooksApi = {
// Список webhooks
list: async (params?: {
event_type?: string;
is_active?: boolean;
limit?: number;
offset?: number;
}): Promise<{ items: Webhook[]; total: number }> => {
const response = await apiClient.get('/webhooks', { params });
return response.data;
},
// Получить webhook
get: async (id: number): Promise<Webhook> => {
const response = await apiClient.get(`/webhooks/${id}`);
return response.data;
},
// Создать webhook
create: async (data: WebhookCreateRequest): Promise<Webhook> => {
const response = await apiClient.post('/webhooks', data);
return response.data;
},
// Обновить webhook
update: async (id: number, data: WebhookUpdateRequest): Promise<Webhook> => {
const response = await apiClient.patch(`/webhooks/${id}`, data);
return response.data;
},
// Удалить webhook
delete: async (id: number): Promise<void> => {
await apiClient.delete(`/webhooks/${id}`);
},
// Статистика
getStats: async (): Promise<{
total_webhooks: number;
active_webhooks: number;
total_deliveries: number;
successful_deliveries: number;
failed_deliveries: number;
success_rate: number;
}> => {
const response = await apiClient.get('/webhooks/stats');
return response.data;
},
// История доставок
getDeliveries: async (
webhookId: number,
params?: { status?: string; limit?: number; offset?: number }
): Promise<{ items: any[]; total: number }> => {
const response = await apiClient.get(`/webhooks/${webhookId}/deliveries`, { params });
return response.data;
},
};
Шаг 2: Список доступных типов событий
// constants/webhookEvents.ts
export const WEBHOOK_EVENT_TYPES = [
{
value: 'user.created',
label: 'Создание пользователя',
description: 'Отправляется при регистрации нового пользователя',
},
{
value: 'payment.completed',
label: 'Завершение платежа',
description: 'Отправляется при успешном пополнении баланса',
},
{
value: 'transaction.created',
label: 'Создание транзакции',
description: 'Отправляется при создании любой транзакции',
},
{
value: 'ticket.created',
label: 'Создание тикета',
description: 'Отправляется при создании нового тикета поддержки',
},
{
value: 'ticket.status_changed',
label: 'Изменение статуса тикета',
description: 'Отправляется при изменении статуса тикета',
},
{
value: 'ticket.message_added',
label: 'Новое сообщение в тикете',
description: 'Отправляется при добавлении нового сообщения в тикет (от пользователя или админа)',
},
] as const;
export type WebhookEventType = typeof WEBHOOK_EVENT_TYPES[number]['value'];
UI компоненты для Webhooks
Шаг 1: Форма создания/редактирования webhook
// components/WebhookForm.tsx
import { useState } from 'react';
import { webhooksApi, WebhookCreateRequest, WebhookUpdateRequest } from '../api/webhooks';
import { WEBHOOK_EVENT_TYPES } from '../constants/webhookEvents';
interface WebhookFormProps {
webhook?: Webhook;
onSuccess: () => void;
onCancel: () => void;
}
function WebhookForm({ webhook, onSuccess, onCancel }: WebhookFormProps) {
const [formData, setFormData] = useState({
name: webhook?.name || '',
url: webhook?.url || '',
event_type: webhook?.event_type || '',
secret: '',
description: webhook?.description || '',
is_active: webhook?.is_active ?? true,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
if (webhook) {
await webhooksApi.update(webhook.id, formData);
} else {
await webhooksApi.create(formData);
}
onSuccess();
} catch (err: any) {
setError(err.response?.data?.detail || 'Ошибка при сохранении webhook');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="webhook-form">
<div className="form-group">
<label>Название *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="form-group">
<label>URL *</label>
<input
type="url"
value={formData.url}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
required
placeholder="https://example.com/webhook"
/>
</div>
<div className="form-group">
<label>Тип события *</label>
<select
value={formData.event_type}
onChange={(e) => setFormData({ ...formData, event_type: e.target.value })}
required
disabled={!!webhook} // Нельзя менять тип события для существующего webhook
>
<option value="">Выберите тип события</option>
{WEBHOOK_EVENT_TYPES.map((event) => (
<option key={event.value} value={event.value}>
{event.label}
</option>
))}
</select>
{formData.event_type && (
<small>
{WEBHOOK_EVENT_TYPES.find((e) => e.value === formData.event_type)?.description}
</small>
)}
</div>
<div className="form-group">
<label>Секрет (опционально)</label>
<input
type="password"
value={formData.secret}
onChange={(e) => setFormData({ ...formData, secret: e.target.value })}
placeholder="Для подписи payload"
/>
<small>Если указан, payload будет подписан с помощью HMAC-SHA256</small>
</div>
<div className="form-group">
<label>Описание</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
/>
</div>
{webhook && (
<div className="form-group">
<label>
<input
type="checkbox"
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
/>
Активен
</label>
</div>
)}
{error && <div className="error-message">{error}</div>}
<div className="form-actions">
<button type="button" onClick={onCancel} disabled={loading}>
Отмена
</button>
<button type="submit" disabled={loading}>
{loading ? 'Сохранение...' : webhook ? 'Обновить' : 'Создать'}
</button>
</div>
</form>
);
}
Шаг 2: Список webhooks
// components/WebhooksList.tsx
import { useState, useEffect } from 'react';
import { webhooksApi, Webhook } from '../api/webhooks';
import WebhookForm from './WebhookForm';
import WebhookDeliveries from './WebhookDeliveries';
function WebhooksList() {
const [webhooks, setWebhooks] = useState<Webhook[]>([]);
const [loading, setLoading] = useState(true);
const [selectedWebhook, setSelectedWebhook] = useState<Webhook | null>(null);
const [showForm, setShowForm] = useState(false);
const [editingWebhook, setEditingWebhook] = useState<Webhook | null>(null);
useEffect(() => {
loadWebhooks();
}, []);
const loadWebhooks = async () => {
try {
setLoading(true);
const data = await webhooksApi.list();
setWebhooks(data.items);
} catch (error) {
console.error('Failed to load webhooks:', error);
} finally {
setLoading(false);
}
};
const handleDelete = async (id: number) => {
if (!confirm('Удалить этот webhook?')) return;
try {
await webhooksApi.delete(id);
loadWebhooks();
} catch (error) {
console.error('Failed to delete webhook:', error);
}
};
const handleToggleActive = async (webhook: Webhook) => {
try {
await webhooksApi.update(webhook.id, { is_active: !webhook.is_active });
loadWebhooks();
} catch (error) {
console.error('Failed to update webhook:', error);
}
};
if (loading) return <div>Загрузка...</div>;
return (
<div className="webhooks-page">
<div className="page-header">
<h1>Webhooks</h1>
<button onClick={() => setShowForm(true)}>Создать webhook</button>
</div>
{showForm && (
<div className="modal">
<div className="modal-content">
<h2>{editingWebhook ? 'Редактировать' : 'Создать'} Webhook</h2>
<WebhookForm
webhook={editingWebhook || undefined}
onSuccess={() => {
setShowForm(false);
setEditingWebhook(null);
loadWebhooks();
}}
onCancel={() => {
setShowForm(false);
setEditingWebhook(null);
}}
/>
</div>
</div>
)}
<table className="webhooks-table">
<thead>
<tr>
<th>Название</th>
<th>URL</th>
<th>Тип события</th>
<th>Статус</th>
<th>Успешно</th>
<th>Ошибок</th>
<th>Последний вызов</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{webhooks.map((webhook) => (
<tr key={webhook.id}>
<td>{webhook.name}</td>
<td>
<code>{webhook.url}</code>
</td>
<td>{webhook.event_type}</td>
<td>
<span className={`status ${webhook.is_active ? 'active' : 'inactive'}`}>
{webhook.is_active ? 'Активен' : 'Неактивен'}
</span>
</td>
<td>{webhook.success_count}</td>
<td className={webhook.failure_count > 0 ? 'error' : ''}>
{webhook.failure_count}
</td>
<td>
{webhook.last_triggered_at
? new Date(webhook.last_triggered_at).toLocaleString()
: 'Никогда'}
</td>
<td>
<button onClick={() => handleToggleActive(webhook)}>
{webhook.is_active ? 'Деактивировать' : 'Активировать'}
</button>
<button onClick={() => {
setEditingWebhook(webhook);
setShowForm(true);
}}>
Редактировать
</button>
<button onClick={() => setSelectedWebhook(webhook)}>
История
</button>
<button onClick={() => handleDelete(webhook.id)} className="danger">
Удалить
</button>
</td>
</tr>
))}
</tbody>
</table>
{selectedWebhook && (
<WebhookDeliveries
webhookId={selectedWebhook.id}
onClose={() => setSelectedWebhook(null)}
/>
)}
</div>
);
}
Шаг 3: История доставок webhook
// components/WebhookDeliveries.tsx
import { useState, useEffect } from 'react';
import { webhooksApi } from '../api/webhooks';
interface WebhookDeliveriesProps {
webhookId: number;
onClose: () => void;
}
function WebhookDeliveries({ webhookId, onClose }: WebhookDeliveriesProps) {
const [deliveries, setDeliveries] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [statusFilter, setStatusFilter] = useState<string>('');
useEffect(() => {
loadDeliveries();
}, [webhookId, statusFilter]);
const loadDeliveries = async () => {
try {
setLoading(true);
const data = await webhooksApi.getDeliveries(webhookId, {
status: statusFilter || undefined,
limit: 50,
});
setDeliveries(data.items);
} catch (error) {
console.error('Failed to load deliveries:', error);
} finally {
setLoading(false);
}
};
return (
<div className="modal">
<div className="modal-content large">
<div className="modal-header">
<h2>История доставок</h2>
<button onClick={onClose}>×</button>
</div>
<div className="filters">
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="">Все статусы</option>
<option value="success">Успешно</option>
<option value="failed">Ошибка</option>
<option value="pending">Ожидает</option>
</select>
</div>
{loading ? (
<div>Загрузка...</div>
) : (
<table>
<thead>
<tr>
<th>Время</th>
<th>Событие</th>
<th>Статус</th>
<th>HTTP код</th>
<th>Ошибка</th>
<th>Попытка</th>
</tr>
</thead>
<tbody>
{deliveries.map((delivery) => (
<tr key={delivery.id}>
<td>{new Date(delivery.created_at).toLocaleString()}</td>
<td>{delivery.event_type}</td>
<td>
<span className={`status ${delivery.status}`}>
{delivery.status}
</span>
</td>
<td>{delivery.response_status || '-'}</td>
<td>
{delivery.error_message ? (
<span className="error" title={delivery.error_message}>
{delivery.error_message.substring(0, 50)}...
</span>
) : (
'-'
)}
</td>
<td>{delivery.attempt_number}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
Обработка ошибок
Обработка ошибок WebSocket
// utils/websocket.ts (дополнение)
class WebSocketManager {
// ... существующий код ...
private handleError(error: Error): void {
console.error('WebSocket error:', error);
// Уведомление пользователя
this.emit('error', {
message: 'Ошибка подключения к серверу',
error: error.message,
});
// Автоматическое переподключение
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnect(this.wsUrl);
}
}
// Показ статуса подключения
getConnectionStatus(): 'connected' | 'disconnected' | 'connecting' {
if (!this.ws) return 'disconnected';
if (this.ws.readyState === WebSocket.OPEN) return 'connected';
if (this.ws.readyState === WebSocket.CONNECTING) return 'connecting';
return 'disconnected';
}
}
Индикатор статуса подключения
// components/ConnectionStatus.tsx
import { useWebSocketContext } from '../contexts/WebSocketContext';
function ConnectionStatus() {
const { wsManager } = useWebSocketContext();
const [status, setStatus] = useState<'connected' | 'disconnected' | 'connecting'>('disconnected');
useEffect(() => {
if (!wsManager) return;
const updateStatus = () => {
setStatus(wsManager.getConnectionStatus());
};
wsManager.on('connected', updateStatus);
wsManager.on('disconnected', updateStatus);
const interval = setInterval(updateStatus, 1000);
return () => {
clearInterval(interval);
wsManager.off('connected', updateStatus);
wsManager.off('disconnected', updateStatus);
};
}, [wsManager]);
return (
<div className={`connection-status ${status}`}>
<span className="status-dot" />
{status === 'connected' && 'Подключено'}
{status === 'disconnected' && 'Отключено'}
{status === 'connecting' && 'Подключение...'}
</div>
);
}
Тестирование
Тестирование WebSocket
-
Проверка подключения:
- Откройте консоль браузера
- Должно появиться сообщение "WebSocket connected"
- Проверьте индикатор статуса подключения
-
Проверка событий:
- Создайте нового пользователя через API или бота
- В консоли должно появиться событие
user.created - Дашборд должен обновиться автоматически
-
Проверка переподключения:
- Остановите сервер
- WebSocket должен отключиться
- Запустите сервер снова
- WebSocket должен автоматически переподключиться
Тестирование Webhooks
-
Создание webhook:
curl -X POST http://localhost:8080/webhooks \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Test Webhook", "url": "https://webhook.site/your-unique-url", "event_type": "user.created" }' -
Проверка доставки:
- Создайте нового пользователя
- Проверьте webhook.site - должен прийти запрос
- Проверьте историю доставок в админке
-
Тестирование подписи:
- Создайте webhook с secret
- Проверьте заголовок
X-Webhook-Signature - Валидируйте подпись на стороне получателя
Чеклист интеграции
- Создан WebSocket менеджер
- WebSocket подключение инициализировано в приложении
- Реализована обработка событий в компонентах
- Добавлены real-time обновления на дашборде
- Реализованы уведомления о новых событиях
- Создан API клиент для webhooks
- Реализована форма создания/редактирования webhooks
- Реализован список webhooks с фильтрацией
- Реализована история доставок
- Добавлена обработка ошибок
- Добавлен индикатор статуса подключения
- Протестированы все функции
Дополнительные рекомендации
-
Оптимизация производительности:
- Используйте debounce для частых обновлений
- Кэшируйте данные, которые не требуют real-time обновлений
- Ограничьте количество одновременно открытых WebSocket соединений
-
Безопасность:
- Всегда используйте HTTPS для webhook URL
- Храните секреты webhooks в безопасном месте
- Валидируйте подпись на стороне получателя
- Ограничьте доступ к управлению webhooks (только для админов)
-
Мониторинг:
- Логируйте все события WebSocket
- Отслеживайте успешность доставки webhooks
- Настройте алерты на большое количество ошибок
-
UX улучшения:
- Показывайте индикатор загрузки при обновлении данных
- Используйте анимации для плавных обновлений
- Предоставьте возможность отключить уведомления
- Добавьте фильтры для событий в уведомлениях