disconnect(); } catch (Exception $e) { // Игнорируем } $redis = null; } try { $host = env('REDIS_HOST', 'crm.clientright.ru'); $port = (int)env('REDIS_PORT', 6379); $password = env('REDIS_PASSWORD', ''); log_message("Подключение к Redis через Predis: $host:$port"); $options = [ 'scheme' => 'tcp', 'host' => $host, 'port' => $port, 'timeout' => 3.0, 'read_write_timeout' => 3.0, ]; if (!empty($password)) { $options['password'] = $password; } $redis = new Predis\Client($options); // Проверяем подключение $pong = $redis->ping(); log_message("Redis (Predis) подключен успешно. Ping: $pong"); } catch (Predis\Connection\ConnectionException $e) { log_message("Ошибка подключения к Redis (Predis): " . $e->getMessage()); $redis = null; return null; } catch (Exception $e) { log_message("Ошибка Redis (Predis): " . $e->getMessage()); $redis = null; return null; } } return $redis; } // Функции для работы с файловым хранилищем (fallback если Redis недоступен) function saveCodeToFile($phone, $code) { $storage_dir = __DIR__ . '/storage/sms_codes'; if (!is_dir($storage_dir)) { @mkdir($storage_dir, 0777, true); @chmod($storage_dir, 0777); } $file = $storage_dir . '/' . md5($phone) . '.json'; $data = [ 'code' => $code, 'phone' => $phone, 'expires' => time() + 600, // 10 минут 'created' => time() ]; $result = @file_put_contents($file, json_encode($data)); if ($result === false) { log_message("Ошибка записи файла: $file. Ошибка: " . (error_get_last()['message'] ?? 'неизвестная')); return false; } @chmod($file, 0666); log_message("Код успешно сохранен в файл: $file"); return true; } function getCodeFromFile($phone) { $storage_dir = __DIR__ . '/storage/sms_codes'; $file = $storage_dir . '/' . md5($phone) . '.json'; log_message("Проверка файла: $file, существует: " . (file_exists($file) ? 'да' : 'нет')); if (!file_exists($file)) { // Проверяем все файлы в директории $files = @scandir($storage_dir); log_message("Файлы в директории: " . json_encode($files)); return false; } $content = @file_get_contents($file); log_message("Содержимое файла: $content"); $data = json_decode($content, true); if (!$data || !isset($data['expires']) || $data['expires'] < time()) { log_message("Код истек или данные некорректны"); @unlink($file); // Удаляем истекший файл return false; } log_message("Код из файла: " . $data['code']); return $data['code']; } function deleteCodeFromFile($phone) { $storage_dir = __DIR__ . '/storage/sms_codes'; $file = $storage_dir . '/' . md5($phone) . '.json'; @unlink($file); return true; } // Очистка номера телефона function clear_phone($phone) { // Убираем все пробелы, скобки, дефисы $phone = preg_replace('/[() -]+/', '', $phone); // Убираем +7 или 8 в начале, оставляем только цифры $phone = preg_replace('/^(\+?7|8)/', '', $phone); return $phone; } // Генерация 6-значного кода function generateCode() { return str_pad(rand(100000, 999999), 6, '0', STR_PAD_LEFT); } // Отправка SMS через n8n webhook function sendSMS($phone, $code) { $webhook_url = env('N8N_SMS_WEBHOOK', ''); if (empty($webhook_url)) { log_message("Ошибка: не указан N8N_SMS_WEBHOOK в .env"); return false; } // Очищаем номер телефона $phone_cleaned = clear_phone($phone); // Формируем текст сообщения $text = "Код подтверждения: $code"; // Данные для отправки в n8n $data = [ 'phone' => $phone_cleaned, 'code' => $code, 'text' => $text, 'timestamp' => date('Y-m-d H:i:s') ]; log_message("Отправка SMS через n8n на номер: $phone_cleaned, код: $code"); // Отправляем запрос на n8n webhook $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $webhook_url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json; charset=utf-8' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); $curl_errno = curl_errno($ch); curl_close($ch); if ($curl_error) { log_message("Ошибка CURL при отправке SMS через n8n: $curl_error (код: $curl_errno)"); return false; } // Логируем ответ от n8n log_message("Ответ от n8n: HTTP $http_code, ответ: " . substr($response, 0, 200)); // Проверяем успешность отправки // n8n может вернуть разные форматы ответов, поэтому проверяем HTTP код if ($http_code >= 200 && $http_code < 300) { // Пытаемся распарсить ответ, если это JSON $response_data = json_decode($response, true); if ($response_data !== null) { // Если есть поле success или status, проверяем его if (isset($response_data['success']) && $response_data['success'] === false) { log_message("n8n вернул ошибку: " . ($response_data['message'] ?? 'неизвестная ошибка')); return false; } if (isset($response_data['status']) && $response_data['status'] === 'error') { log_message("n8n вернул статус ошибки: " . ($response_data['message'] ?? 'неизвестная ошибка')); return false; } } log_message("SMS отправлено успешно через n8n на номер: $phone_cleaned"); return true; } else { log_message("Ошибка отправки SMS через n8n. HTTP код: $http_code, Ответ: " . substr($response, 0, 200)); return false; } } // Проверка rate limiting function checkRateLimit($phone, $redis) { if (!$redis) { return true; // Если Redis недоступен, пропускаем проверку } $phone_cleaned = clear_phone($phone); $key_send = "sms:ratelimit:send:$phone_cleaned"; $key_attempts = "sms:ratelimit:attempts:$phone_cleaned"; // Проверяем количество отправок за последний час (максимум 5) $send_count = $redis->get($key_send); if ($send_count && $send_count >= 5) { return false; } // Проверяем количество попыток проверки за последние 15 минут (максимум 10) $attempts_count = $redis->get($key_attempts); if ($attempts_count && $attempts_count >= 10) { return false; } return true; } // Обработка запросов $action = $_GET['action'] ?? $_POST['action'] ?? ''; try { if ($action === 'send') { // Отправка SMS кода $phone = $_POST['phonenumber'] ?? ''; log_message("=== ОТПРАВКА SMS ==="); log_message("Входящий номер (raw): '$phone'"); log_message("POST данные: " . json_encode($_POST, JSON_UNESCAPED_UNICODE)); if (empty($phone)) { throw new Exception("Номер телефона не указан"); } $phone_cleaned = clear_phone($phone); log_message("Номер после очистки: '$phone_cleaned'"); // Проверка rate limiting $redis = getRedis(); if ($redis && !checkRateLimit($phone_cleaned, $redis)) { throw new Exception("Превышен лимит запросов. Попробуйте позже."); } // Генерируем код $code = generateCode(); $code_saved = false; // Сохраняем код в Redis на 10 минут ПЕРЕД отправкой SMS if ($redis) { try { $key = "sms:code:$phone_cleaned"; $saved = $redis->setex($key, 600, $code); // 10 минут if (!$saved) { log_message("Ошибка: не удалось сохранить код в Redis для номера $phone_cleaned"); } else { log_message("Код сохранен в Redis для номера $phone_cleaned: $code"); $code_saved = true; // Увеличиваем счетчик отправок $key_send = "sms:ratelimit:send:$phone_cleaned"; $current = $redis->get($key_send) ?: 0; $redis->setex($key_send, 3600, $current + 1); // 1 час } } catch (RedisException $e) { log_message("Ошибка Redis при сохранении кода: " . $e->getMessage()); } } // Если Redis недоступен или не удалось сохранить, сохраняем в файл (fallback) if (!$code_saved) { log_message("Сохранение кода в файл (fallback) для номера $phone_cleaned"); saveCodeToFile($phone_cleaned, $code); $code_saved = true; } // Отправляем SMS $sms_sent = sendSMS($phone, $code); if (!$sms_sent) { // Если SMS не отправилось, удаляем код из Redis и файла if ($redis) { try { $key = "sms:code:$phone_cleaned"; $deleted = $redis->del($key); log_message("SMS не отправлено, код удален из Redis (ключ: $key, удалено: $deleted)"); } catch (Exception $e) { log_message("Ошибка при удалении кода из Redis: " . $e->getMessage()); } } deleteCodeFromFile($phone_cleaned); // Удаляем из файла тоже throw new Exception("Не удалось отправить SMS"); } $storage_info = $code_saved ? ($redis ? "Redis (ключ: sms:code:$phone_cleaned)" : "файловое хранилище") : "не сохранен"; log_message("Код отправлен на номер: $phone_cleaned, код сохранен в: $storage_info"); echo json_encode([ 'success' => true, 'message' => 'Код отправлен на ваш номер телефона' ], JSON_UNESCAPED_UNICODE); } elseif ($action === 'verify') { // Проверка кода через n8n webhook log_message("=== НАЧАЛО ПРОВЕРКИ КОДА ==="); log_message("REQUEST_METHOD: " . ($_SERVER['REQUEST_METHOD'] ?? 'не установлен')); log_message("POST данные: " . json_encode($_POST, JSON_UNESCAPED_UNICODE)); $phone = $_POST['phonenumber'] ?? ''; $code = $_POST['code'] ?? ''; log_message("Входящий номер (raw): '$phone'"); log_message("Входящий код: '$code'"); if (empty($phone) || empty($code)) { log_message("Ошибка: номер телефона или код не указаны"); throw new Exception("Номер телефона или код не указаны"); } // Получаем URL webhook из .env $webhook_url = env('N8N_SMS_VERIFY_WEBHOOK', ''); if (empty($webhook_url)) { log_message("Ошибка: не указан N8N_SMS_VERIFY_WEBHOOK в .env"); throw new Exception("Сервис временно недоступен. Попробуйте позже."); } log_message("Отправка запроса на n8n webhook: $webhook_url"); // Отправляем запрос на n8n webhook $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $webhook_url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'phonenumber' => $phone, 'code' => $code ], JSON_UNESCAPED_UNICODE)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json; charset=utf-8' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_error = curl_error($ch); $curl_errno = curl_errno($ch); curl_close($ch); if ($curl_error) { log_message("Ошибка CURL при проверке кода через n8n: $curl_error (код: $curl_errno)"); throw new Exception("Ошибка соединения с сервисом. Попробуйте позже."); } // Логируем ответ от n8n log_message("Ответ от n8n: HTTP $http_code, ответ: " . substr($response, 0, 200)); // Парсим ответ $response_data = json_decode($response, true); if ($response_data === null) { log_message("Ошибка: не удалось распарсить ответ от n8n: " . substr($response, 0, 200)); throw new Exception("Ошибка обработки ответа. Попробуйте позже."); } // Проверяем успешность if ($http_code >= 200 && $http_code < 300) { // Успешный ответ от n8n if (isset($response_data['success']) && $response_data['success'] === true) { log_message("Код подтвержден через n8n для номера: " . ($phone ?: 'не указан')); // Возвращаем упрощенный ответ (без токена) echo json_encode([ 'success' => true, 'message' => $response_data['message'] ?? 'Код подтвержден' ], JSON_UNESCAPED_UNICODE); } else { // Ошибка от n8n $error_message = $response_data['message'] ?? 'Неверный код'; log_message("Ошибка проверки кода через n8n: $error_message"); http_response_code(400); echo json_encode([ 'success' => false, 'message' => $error_message ], JSON_UNESCAPED_UNICODE); } } else { // HTTP ошибка $error_message = $response_data['message'] ?? "Ошибка сервиса (HTTP $http_code)"; log_message("HTTP ошибка от n8n: $http_code, сообщение: $error_message"); http_response_code($http_code >= 500 ? 500 : 400); echo json_encode([ 'success' => false, 'message' => $error_message ], JSON_UNESCAPED_UNICODE); } } elseif ($action === 'check_verified') { // Проверка статуса верификации (для проверки перед отправкой формы) $phone = $_POST['phonenumber'] ?? ''; $token = $_POST['token'] ?? ''; if (empty($phone) || empty($token)) { throw new Exception("Данные не указаны"); } $phone_cleaned = clear_phone($phone); $redis = getRedis(); if (!$redis) { throw new Exception("Сервис временно недоступен"); } $verify_key = "sms:verified:$phone_cleaned"; $stored_token = $redis->get($verify_key); if ($stored_token === null || $stored_token !== $token) { echo json_encode([ 'success' => false, 'verified' => false ], JSON_UNESCAPED_UNICODE); } else { echo json_encode([ 'success' => true, 'verified' => true ], JSON_UNESCAPED_UNICODE); } } else { throw new Exception("Неизвестное действие"); } } catch (Exception $e) { log_message("Ошибка: " . $e->getMessage()); http_response_code(400); echo json_encode([ 'success' => false, 'message' => $e->getMessage() ], JSON_UNESCAPED_UNICODE); }