Tabela de conteúdos
Passo 03: Sistema de Validação e Middleware - Telegram Webhook
⚠️ INSTRUÇÕES IMPORTANTES ANTES DE COMEÇAR
Para evitar erros durante a implementação, siga EXATAMENTE estas etapas:
✅ 1. Ler o template COMPLETO da página
- Leia toda a documentação antes de começar
- Entenda o fluxo completo de implementação
- Identifique dependências entre os passos
✅ 2. Identificar TODOS os arquivos mencionados
- Liste todos os arquivos que serão criados/modificados
- Verifique se já existem no projeto
- Anote o caminho exato de cada arquivo
✅ 3. Verificar a estrutura EXATA de cada arquivo
- Confirme namespaces e imports corretos
- Verifique se dependências estão instaladas
- Valide sintaxe PHP antes de implementar
✅ 4. Implementar linha por linha conforme template
- Copie o código EXATAMENTE como mostrado
- Não modifique namespaces ou imports
- Execute comandos na ordem especificada
🚨 ATENÇÃO:
- NÃO pule etapas - cada passo tem dependências
- NÃO modifique o código fornecido sem entender as consequências
- SEMPRE teste após cada implementação
- MANTENHA backup antes de grandes alterações
📋 Visão Geral
Sistema de validação e middleware que garante segurança, tratamento de exceções e logging para o Telegram Webhook. Depende dos sistemas de logging, rastreamento de fluxo e canais de notificação.
🚀 Comando de Implementação
implementar sistema validacao middleware telegram
⚙️ Pré-requisitos
- Sistema de Logging implementado
- Sistema de Rastreamento de Fluxo implementado
- Sistema de Canais de Notificação implementado
- Laravel 12 instalado
- PHP 8.2+ configurado
📁 Arquivos do Módulo
🛡️ Middleware
- `app/Http/Middleware/TelegramWebhookSecretMiddleware.php`
- Validação de secret do webhook
- Segurança contra requisições não autorizadas
- Rate limiting
- `app/Http/Middleware/TelegramWebhookExceptionHandler.php`
- Tratamento de exceções específicas
- Notificações de erro via Telegram
- Logging de erros críticos
- `app/Http/Middleware/TelegramWebhookLoggingMiddleware.php`
- Logging de requisições
- Rastreamento de performance
- Auditoria de acesso
🔧 Services
- `app/Services/Telegram/TelegramWebhookValidationService.php`
- Validação de payloads do webhook
- Detecção de duplicatas
- Validação de estrutura de dados
📝 Requests
- `app/Http/Requests/TelegramWebhookRequest.php`
- Validação de dados de entrada
- Sanitização de payloads
- Validação de permissões
- `app/Http/Requests/TelegramWebhookSetupRequest.php`
- Validação de configuração de webhook
- Validação de URLs
- Validação de permissões de admin
🧪 Testes
- `tests/Unit/TelegramWebhookValidationServiceTest.php`
- Testes unitários de validação
- Testes de detecção de duplicatas
- Testes de estrutura de dados
- `tests/Unit/TelegramWebhookMiddlewareTest.php`
- Testes de middleware
- Testes de segurança
- Testes de rate limiting
🔧 Implementação Passo a Passo
- Importante orientação: implemente apenas as primeiras linhas de cada classe, pois, serão completadas posteriormente.
Passo 1: Criar Middleware de Secret
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use App\Contracts\LoggingServiceInterface; use Symfony\Component\HttpFoundation\Response; class TelegramWebhookSecretMiddleware { public function __construct( private LoggingServiceInterface $loggingService ) {} /** * Handle an incoming request. * * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next): Response { $secretToken = config('services.telegram.webhook_secret'); // Se não há secret configurado, permite a requisição (modo de desenvolvimento) if (empty($secretToken)) { $this->loggingService->logTelegramEvent('webhook_secret_not_configured', [ 'warning' => 'Webhook secret not configured - allowing all requests', 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), 'timestamp' => now()->toISOString() ], 'warning'); return $next($request); } // Verificar se o header do secret está presente $incomingSecret = $request->header('X-Telegram-Bot-Api-Secret-Token'); if (empty($incomingSecret)) { $this->loggingService->logTelegramEvent('webhook_secret_missing', [ 'error' => 'Missing webhook secret token in request', 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), 'headers' => $request->headers->all(), 'timestamp' => now()->toISOString() ], 'error'); return response()->json([ 'error' => 'Unauthorized - Missing secret token', 'message' => 'Webhook secret token is required' ], 401); } // Verificar se o secret é válido if (!hash_equals($secretToken, $incomingSecret)) { $this->loggingService->logTelegramEvent('webhook_secret_invalid', [ 'error' => 'Invalid webhook secret token', 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), 'incoming_secret' => substr($incomingSecret, 0, 8) . '...', // Log apenas parte do secret por segurança 'expected_secret_prefix' => substr($secretToken, 0, 8) . '...', 'timestamp' => now()->toISOString() ], 'error'); return response()->json([ 'error' => 'Unauthorized - Invalid secret token', 'message' => 'Webhook secret token is invalid' ], 401); } return $next($request); } }
Passo 2: Criar Middleware de Exceções
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use App\Services\Channels\TelegramChannel; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\Response; class TelegramWebhookExceptionHandler { public function __construct( private TelegramChannel $telegramChannel ) {} /** * Handle an incoming request. */ public function handle(Request $request, Closure $next): Response { try { return $next($request); } catch (ValidationException $e) { // Extract chat_id from the request if available $chatId = $this->extractChatId($request); if ($chatId) { $this->sendValidationErrorToTelegram($chatId, $e); } // Log the validation error Log::warning('Telegram webhook validation failed', [ 'chat_id' => $chatId, 'errors' => $e->errors(), 'payload' => $request->all() ]); // Return a proper response to Telegram (200 OK to acknowledge receipt) return response()->json([ 'status' => 'error', 'message' => 'Validation failed', 'errors' => $e->errors() ], 200)->header('Content-Type', 'application/json'); } catch (\Exception $e) { // Extract chat_id from the request if available $chatId = $this->extractChatId($request); if ($chatId) { $this->sendGeneralErrorToTelegram($chatId, $e); } // Log the general error Log::error('Telegram webhook general error', [ 'chat_id' => $chatId, 'error' => $e->getMessage(), 'payload' => $request->all() ]); // Return a proper response to Telegram (200 OK to acknowledge receipt) return response()->json([ 'status' => 'error', 'message' => 'Internal server error' ], 200)->header('Content-Type', 'application/json'); } } /** * Extract chat_id from the request payload */ private function extractChatId(Request $request): ?string { $payload = $request->all(); // Try to get chat_id from message if (isset($payload['message']['chat']['id'])) { return (string) $payload['message']['chat']['id']; } // Try to get chat_id from callback_query if (isset($payload['callback_query']['message']['chat']['id'])) { return (string) $payload['callback_query']['message']['chat']['id']; } return null; } /** * Send validation error message to Telegram chat */ private function sendValidationErrorToTelegram(string $chatId, ValidationException $e): void { try { $errorMessage = $this->formatValidationErrorMessage($e); $this->telegramChannel->sendTextMessage($errorMessage, $chatId); Log::info('Validation error sent to Telegram chat', [ 'chat_id' => $chatId, 'error_count' => count($e->errors()) ]); } catch (\Exception $telegramError) { Log::error('Failed to send validation error to Telegram', [ 'chat_id' => $chatId, 'telegram_error' => $telegramError->getMessage(), 'original_validation_error' => $e->getMessage() ]); } } /** * Send general error message to Telegram chat */ private function sendGeneralErrorToTelegram(string $chatId, \Exception $e): void { try { $errorMessage = $this->formatGeneralErrorMessage($e); $this->telegramChannel->sendTextMessage($errorMessage, $chatId); Log::info('General error sent to Telegram chat', [ 'chat_id' => $chatId, 'error' => $e->getMessage() ]); } catch (\Exception $telegramError) { Log::error('Failed to send general error to Telegram', [ 'chat_id' => $chatId, 'telegram_error' => $telegramError->getMessage(), 'original_error' => $e->getMessage() ]); } } /** * Format validation error message for Telegram */ private function formatValidationErrorMessage(ValidationException $e): string { $message = "❌ *Erro de Validação*\n\n"; $message .= "Ocorreu um erro ao processar sua mensagem:\n\n"; foreach ($e->errors() as $field => $errors) { $fieldName = $this->getFieldDisplayName($field); $message .= "• *{$fieldName}:* " . implode(', ', $errors) . "\n"; } $message .= "\nPor favor, tente novamente com os dados corretos."; return $message; } /** * Format general error message for Telegram */ private function formatGeneralErrorMessage(\Exception $e): string { $message = "⚠️ *Erro do Sistema*\n\n"; $message .= "Ocorreu um erro inesperado ao processar sua solicitação.\n\n"; $message .= "Por favor, tente novamente em alguns instantes.\n"; $message .= "Se o problema persistir, entre em contato com o suporte."; return $message; } /** * Get user-friendly field names for validation errors */ private function getFieldDisplayName(string $field): string { $fieldNames = [ 'update_id' => 'ID da Atualização', 'message' => 'Mensagem', 'callback_query' => 'Consulta de Callback', 'message.chat.id' => 'ID do Chat', 'message.text' => 'Texto da Mensagem', 'message.from.id' => 'ID do Usuário', 'callback_query.id' => 'ID da Consulta', 'callback_query.data' => 'Dados da Consulta', 'callback_query.message.chat.id' => 'ID do Chat (Callback)', ]; return $fieldNames[$field] ?? $field; } }
Passo 3: Criar Middleware de Logging
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use App\Contracts\LoggingServiceInterface; use Symfony\Component\HttpFoundation\Response; class TelegramWebhookLoggingMiddleware { public function __construct( private LoggingServiceInterface $loggingService ) {} /** * Handle an incoming request. * * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next): Response { // Log the raw request before any validation $this->loggingService->logTelegramEvent('telegram_webhook_raw_received', [ 'method' => $request->method(), 'url' => $request->fullUrl(), 'headers' => $request->headers->all(), 'raw_body' => $request->getContent(), 'all_data' => $request->all(), 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), 'timestamp' => now()->toISOString(), ], 'info'); return $next($request); } }
Passo 4: Criar Service de Validação
<?php namespace App\Services\Telegram; use App\Contracts\LoggingServiceInterface; use Illuminate\Support\Facades\Cache; class TelegramWebhookValidationService { private const CACHE_PREFIX = 'telegram_update_processed_'; private const CACHE_TTL = 300; // 5 minutes public function __construct( private LoggingServiceInterface $loggingService ) {} /** * Check if webhook is duplicate and mark as processing */ public function validateAndMarkProcessing(?int $updateId, bool $skipDuplicateCheck = false): bool { if (!$updateId) { return true; // No update_id, allow processing } // Skip duplicate check if requested (useful for testing) if ($skipDuplicateCheck) { $this->markRequestAsProcessing($updateId); return true; } // Check if already processed if ($this->isDuplicateRequest($updateId)) { $this->logDuplicateWebhook($updateId); return false; } // Mark as processing to prevent race conditions $this->markRequestAsProcessing($updateId); return true; } /** * Check if request is duplicate (quick cache check) */ private function isDuplicateRequest(int $updateId): bool { try { $cacheKey = $this->getCacheKey($updateId); return Cache::has($cacheKey); } catch (\Exception $e) { // If cache fails, log but don't block processing $this->loggingService->logException($e, [ 'operation' => 'check_duplicate_update_id', 'update_id' => $updateId ]); return false; } } /** * Mark request as being processed (to prevent race conditions) */ private function markRequestAsProcessing(int $updateId): void { try { $cacheKey = $this->getCacheKey($updateId); Cache::put($cacheKey, true, self::CACHE_TTL); } catch (\Exception $e) { // If cache fails, log but don't block processing $this->loggingService->logException($e, [ 'operation' => 'mark_update_id_processed', 'update_id' => $updateId ]); } } /** * Get cache key for update_id */ private function getCacheKey(int $updateId): string { return self::CACHE_PREFIX . $updateId; } /** * Log duplicate webhook detection */ private function logDuplicateWebhook(int $updateId): void { $this->loggingService->logTelegramEvent('duplicate_webhook_ignored_early', [ 'update_id' => $updateId, 'message' => 'Duplicate webhook detected and ignored before processing' ], 'info'); } /** * Clean up processed update_id (useful for testing or manual cleanup) */ public function cleanupProcessedUpdate(int $updateId): bool { try { $cacheKey = $this->getCacheKey($updateId); return Cache::forget($cacheKey); } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'cleanup_processed_update_id', 'update_id' => $updateId ]); return false; } } /** * Get cache statistics for monitoring */ public function getCacheStats(): array { try { // This is a simplified approach - in production you might want more sophisticated stats return [ 'cache_driver' => config('cache.default'), 'cache_prefix' => self::CACHE_PREFIX, 'cache_ttl' => self::CACHE_TTL, 'note' => 'Use Redis or Memcached for better performance and monitoring' ]; } catch (\Exception $e) { return [ 'error' => 'Failed to get cache stats: ' . $e->getMessage() ]; } } }
Passo 5: Criar Requests
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use App\Contracts\LoggingServiceInterface; class TelegramWebhookRequest extends FormRequest { public function __construct( private LoggingServiceInterface $loggingService ) { parent::__construct(); } /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; // Webhook requests are always authorized } /** * Get the validation rules that apply to the request. */ public function rules(): array { return [ 'update_id' => 'required|integer', 'message' => 'sometimes|array', 'callback_query' => 'sometimes|array', 'message.chat.id' => 'required_with:message|integer', 'message.from.id' => 'required_with:message|integer', // Remove required validation for text since audio/voice messages don't have text 'message.text' => 'sometimes|string', // Add validation for voice messages 'message.voice' => 'sometimes|array', 'message.voice.file_id' => 'required_with:message.voice|string', 'message.voice.duration' => 'sometimes|integer', 'message.voice.mime_type' => 'sometimes|string', // Add validation for audio messages 'message.audio' => 'sometimes|array', 'message.audio.file_id' => 'required_with:message.audio|string', 'message.audio.duration' => 'sometimes|integer', 'message.audio.title' => 'sometimes|string', 'message.audio.performer' => 'sometimes|string', // Add validation for other message types 'message.photo' => 'sometimes|array', 'message.document' => 'sometimes|array', 'message.video' => 'sometimes|array', 'message.sticker' => 'sometimes|array', 'message.location' => 'sometimes|array', 'message.contact' => 'sometimes|array', 'callback_query.id' => 'required_with:callback_query|string', 'callback_query.data' => 'required_with:callback_query|string', 'callback_query.message.chat.id' => 'required_with:callback_query|integer', ]; } /** * Get custom messages for validator errors. */ public function messages(): array { return [ 'update_id.required' => 'Update ID is required', 'update_id.integer' => 'Update ID must be an integer', 'message.array' => 'Message must be an array', 'callback_query.array' => 'Callback query must be an array', 'message.chat.id.required_with' => 'Chat ID is required when message is present', 'message.chat.id.integer' => 'Chat ID must be an integer', 'message.from.id.required_with' => 'User ID is required when message is present', 'message.from.id.integer' => 'User ID must be an integer', 'message.text.string' => 'Message text must be a string', 'message.voice.array' => 'Voice message must be an array', 'message.voice.file_id.required_with' => 'Voice file ID is required when voice message is present', 'message.voice.file_id.string' => 'Voice file ID must be a string', 'message.voice.duration.integer' => 'Voice duration must be an integer', 'message.voice.mime_type.string' => 'Voice MIME type must be a string', 'message.audio.array' => 'Audio message must be an array', 'message.audio.file_id.required_with' => 'Audio file ID is required when audio message is present', 'message.audio.file_id.string' => 'Audio file ID must be a string', 'message.audio.duration.integer' => 'Audio duration must be an integer', 'message.audio.title.string' => 'Audio title must be a string', 'message.audio.performer.string' => 'Audio performer must be a string', 'callback_query.id.required_with' => 'Callback query ID is required when callback query is present', 'callback_query.data.required_with' => 'Callback data is required when callback query is present', 'callback_query.message.chat.id.required_with' => 'Chat ID is required when callback query is present', ]; } /** * Determine if the request expects JSON. * * @return bool */ public function expectsJson(): bool { return true; // Always expect JSON for webhook requests } /** * Determine if the request is asking for JSON. * * @return bool */ public function wantsJson(): bool { return true; // Always want JSON for webhook requests } /** * Handle a failed validation attempt. * Instead of throwing HTTP exception, we'll let the controller handle it * and send a friendly message via Telegram. */ protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator): void { // Log detalhes da validação falhada $this->loggingService->logTelegramEvent('telegram_webhook_validation_failed', [ 'error' => 'Webhook validation failed', 'validation_errors' => $validator->errors()->toArray(), 'request_data' => $this->all(), 'request_headers' => $this->headers->all(), 'ip' => $this->ip(), 'user_agent' => $this->userAgent(), 'timestamp' => now()->toISOString(), 'validation_rules' => $this->rules(), 'failed_fields' => array_keys($validator->errors()->toArray()), 'request_size' => strlen($this->getContent()), 'content_type' => $this->header('Content-Type'), 'telegram_update_id' => $this->input('update_id'), 'message_type' => $this->getMessageType(), 'has_message' => $this->has('message'), 'has_callback_query' => $this->has('callback_query') ], 'error'); // Store validation errors in the request for the controller to handle $this->merge(['validation_errors' => $validator->errors()->toArray()]); // Don't throw exception - let controller handle it gracefully // parent::failedValidation($validator); } /** * Get the message type from the request */ private function getMessageType(): string { if ($this->has('callback_query')) { return 'callback_query'; } if ($this->has('message')) { $message = $this->input('message', []); if (isset($message['text'])) { return 'text_message'; } if (isset($message['voice'])) { return 'voice_message'; } if (isset($message['audio'])) { return 'audio_message'; } if (isset($message['photo'])) { return 'photo_message'; } if (isset($message['document'])) { return 'document_message'; } if (isset($message['video'])) { return 'video_message'; } if (isset($message['sticker'])) { return 'sticker_message'; } if (isset($message['location'])) { return 'location_message'; } if (isset($message['contact'])) { return 'contact_message'; } return 'unknown_message_type'; } return 'no_message'; } /** * Log validation attempt for debugging */ public function validateResolved(): void { // Log successful validation // $this->loggingService->logTelegramEvent('telegram_webhook_validation_success', [ // 'success' => 'Webhook validation passed successfully', // 'message_type' => $this->getMessageType(), // 'telegram_update_id' => $this->input('update_id'), // 'has_message' => $this->has('message'), // 'has_callback_query' => $this->has('callback_query'), // 'message_content_types' => $this->getMessageContentTypes(), // 'timestamp' => now()->toISOString() // ], 'info'); parent::validateResolved(); } /** * Get all content types present in the message */ private function getMessageContentTypes(): array { if (!$this->has('message')) { return []; } $message = $this->input('message', []); $contentTypes = []; if (isset($message['text'])) { $contentTypes[] = 'text'; } if (isset($message['voice'])) { $contentTypes[] = 'voice'; } if (isset($message['audio'])) { $contentTypes[] = 'audio'; } if (isset($message['photo'])) { $contentTypes[] = 'photo'; } if (isset($message['document'])) { $contentTypes[] = 'document'; } if (isset($message['video'])) { $contentTypes[] = 'video'; } if (isset($message['sticker'])) { $contentTypes[] = 'sticker'; } if (isset($message['location'])) { $contentTypes[] = 'location'; } if (isset($message['contact'])) { $contentTypes[] = 'contact'; } return $contentTypes; } }
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class TelegramWebhookSetupRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. */ public function rules(): array { return [ 'webhook_url' => 'required|url|max:255', ]; } /** * Get custom messages for validator errors. */ public function messages(): array { return [ 'webhook_url.required' => 'Webhook URL is required', 'webhook_url.url' => 'Webhook URL must be a valid URL', 'webhook_url.max' => 'Webhook URL cannot exceed 255 characters', ]; } }
Passo 6: Registrar Middleware
// app/Http/Kernel.php protected $middlewareAliases = [ // ... outros middleware 'telegram.webhook.secret' => \App\Http\Middleware\TelegramWebhookSecretMiddleware::class, 'telegram.webhook.exception' => \App\Http\Middleware\TelegramWebhookExceptionHandler::class, 'telegram.webhook.logging' => \App\Http\Middleware\TelegramWebhookLoggingMiddleware::class, ];
🧪 Testes
Teste Unitário de Validação
<?php namespace Tests\Unit\Services\Telegram; use App\Services\Telegram\TelegramWebhookValidationService; use App\Contracts\LoggingServiceInterface; use Mockery; use PHPUnit\Framework\TestCase; class TelegramWebhookValidationServiceTest extends TestCase { private TelegramWebhookValidationService $service; private LoggingServiceInterface $loggingService; protected function setUp(): void { parent::setUp(); $this->loggingService = Mockery::mock(LoggingServiceInterface::class); $this->service = new TelegramWebhookValidationService($this->loggingService); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } public function test_validate_and_mark_processing_without_update_id_returns_true() { $result = $this->service->validateAndMarkProcessing(null); $this->assertTrue($result); } public function test_service_can_be_instantiated() { $this->assertInstanceOf(TelegramWebhookValidationService::class, $this->service); } public function test_service_has_required_methods() { $this->assertTrue(method_exists($this->service, 'validateAndMarkProcessing')); $this->assertTrue(method_exists($this->service, 'cleanupProcessedUpdate')); $this->assertTrue(method_exists($this->service, 'getCacheStats')); } }
✅ Validação do Módulo
Checklist de Implementação
- [ ] Middleware `TelegramWebhookSecretMiddleware` criado
- [ ] Middleware `TelegramWebhookExceptionHandler` criado
- [ ] Middleware `TelegramWebhookLoggingMiddleware` criado
- [ ] Service `TelegramWebhookValidationService` implementado
- [ ] Request `TelegramWebhookRequest` criado
- [ ] Request `TelegramWebhookSetupRequest` criado
- [ ] Middleware registrado no Kernel
- [ ] Testes unitários implementados
- [ ] Integração com sistemas anteriores
Comandos de Validação
# Executar testes php artisan test tests/Unit/TelegramWebhookValidationServiceTest.php # Verificar middleware registrado php artisan route:list --middleware=telegram.webhook.secret # Testar validação php artisan tinker # >>> $service = app(\App\Services\Telegram\TelegramWebhookValidationService::class); # >>> $service->validateWebhookPayload(['update_id' => 123, 'message' => ['message_id' => 1, 'from' => ['id' => 123], 'chat' => ['id' => 123], 'date' => time()]]);
