enforce(RateLimiter::identifier(), 'otp'); // 2. Fetch input parameters $receiver = filterRequest("receiver"); if (empty($receiver)) { $receiver = filterRequest("phone_number"); } $user_type = filterRequest("user_type"); $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? (function_exists('apache_request_headers') ? (apache_request_headers()['Authorization'] ?? null) : null); if (!empty($authHeader) && preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { $jwtToken = $matches[1]; $tokenParts = explode('.', $jwtToken); if (count($tokenParts) === 3) { $payload = json_decode(base64_decode($tokenParts[1]), true); if (isset($payload['role'])) { $user_type = $payload['role']; } } } $country = filterRequest("country"); // Egypt | Syria | Jordan $method = filterRequest("method"); // whatsapp | sms | voice | flash_call | bearer_send $context = filterRequest("context"); // token_change | login (default) // For driver registration context $driverId = filterRequest("driverId"); $email = filterRequest("email"); if (empty($receiver)) { jsonError("Phone number (receiver) is required."); exit; } // Auto-detect country if empty if (empty($country)) { $cleanReceiver = preg_replace('/\D+/', '', $receiver); if (strpos($cleanReceiver, '20') === 0 || (strlen($cleanReceiver) === 11 && strpos($cleanReceiver, '01') === 0)) { $country = 'Egypt'; } elseif (strpos($cleanReceiver, '962') === 0 || (strlen($cleanReceiver) === 9 && strpos($cleanReceiver, '7') === 0)) { $country = 'Jordan'; } elseif (strpos($cleanReceiver, '963') === 0 || (strlen($cleanReceiver) === 9 && strpos($cleanReceiver, '9') === 0)) { $country = 'Syria'; } else { $country = 'Jordan'; // Default fallback } } // Auto-detect user_type if empty if (empty($user_type)) { if (!empty($driverId) || strpos($_SERVER['REQUEST_URI'], 'driver') !== false) { $user_type = 'driver'; } else { $user_type = 'passenger'; } } if (empty($user_type) || !in_array($user_type, ['passenger', 'driver', 'admin', 'service'])) { jsonError("User type must be 'passenger', 'driver', 'admin', or 'service'."); exit; } if ($user_type === 'admin') { $allowedPhones = explode(',', getenv('ADMIN_PHONE_NUMBERS')); if (!in_array($receiver, $allowedPhones)) { error_log("⚠️ [Admin OTP] Unauthorized phone number attempted: $receiver"); jsonError("رقم الهاتف غير مصرح له."); exit; } } // 3. Establish DB Connection try { $con = Database::get('main'); } catch (Exception $e) { http_response_code(500); exit(json_encode(['error' => 'Database connection failed'])); } // 4. Generate 3-digit OTP code $otp = str_pad((string)random_int(0, 999), 3, '0', STR_PAD_LEFT); // 5. Geographical Routing & Dispatch $sentSuccessfully = false; switch (strtolower($country)) { case 'egypt': $sentSuccessfully = sendKazumiSms($receiver, $otp); if (!$sentSuccessfully) { error_log("⚠️ [Egypt OTP Failover 1] Kazumi SMS failed. Falling back to Intaleq OTP WhatsApp."); $sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp'); if (!$sentSuccessfully) { error_log("⚠️ [Egypt OTP Failover 2] Intaleq OTP WhatsApp failed. Falling back to Nabeh JWT OTP text."); $sentSuccessfully = sendNabehOtp($receiver, $otp, 'text'); } } break; case 'syria': // Syria uses dynamic Nabeh JWT for voice/image, static Intaleq app_key for whatsapp/sms if ($method === 'bearer_send' || $method === 'voice' || $method === 'image') { $sentSuccessfully = sendNabehOtp($receiver, $otp, $method); if (!$sentSuccessfully) { error_log("⚠️ [Syria OTP Failover] Nabeh JWT method failed. Falling back to Intaleq OTP WhatsApp."); $sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp'); } } else { $sentSuccessfully = sendIntaleqOtp($receiver, $otp, $method ?: 'whatsapp'); if (!$sentSuccessfully) { error_log("⚠️ [Syria OTP Failover] Intaleq OTP WhatsApp failed. Falling back to Nabeh JWT OTP text."); $sentSuccessfully = sendNabehOtp($receiver, $otp, 'text'); } } break; case 'jordan': // Jordan uses dynamic Nabeh JWT for voice/image, static Intaleq app_key for sms/whatsapp if ($method === 'bearer_send' || $method === 'voice' || $method === 'image') { $sentSuccessfully = sendNabehOtp($receiver, $otp, $method); if (!$sentSuccessfully) { error_log("⚠️ [Jordan OTP Failover] Nabeh JWT method failed. Falling back to Intaleq OTP SMS."); $sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'sms'); } } else { $sentSuccessfully = sendIntaleqOtp($receiver, $otp, $method ?: 'sms'); if (!$sentSuccessfully) { error_log("⚠️ [Jordan OTP Failover] Intaleq OTP SMS failed. Falling back to Nabeh JWT OTP text."); $sentSuccessfully = sendNabehOtp($receiver, $otp, 'text'); } } break; default: // Default fallback to Kazumi SMS $sentSuccessfully = sendKazumiSms($receiver, $otp); if (!$sentSuccessfully) { error_log("⚠️ [Default OTP Failover] Kazumi SMS failed. Falling back to Intaleq OTP WhatsApp."); $sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp'); } break; } // 6. DB Storage on Success if ($sentSuccessfully) { $encryptedPhone = $encryptionHelper->encryptData($receiver); $encryptedOtp = $encryptionHelper->encryptData($otp); $encryptedEmail = !empty($email) ? $encryptionHelper->encryptData($email) : ''; $expirationTime = date('Y-m-d H:i:s', strtotime('+5 minutes')); try { if ($user_type === 'admin') { $stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE)) ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)"); $stmt->execute([$encryptedPhone, $encryptedOtp]); } elseif ($user_type === 'service') { $stmtDel = $con->prepare("DELETE FROM `phone_verification_service` WHERE `phone_number` = ?"); $stmtDel->execute([$encryptedPhone]); $stmtIns = $con->prepare(" INSERT INTO `phone_verification_service` (`phone_number`, `token_code`, `expiration_time`, `is_verified`, `created_at`) VALUES (?, ?, ?, 0, NOW()) "); $stmtIns->execute([ $encryptedPhone, $encryptedOtp, $expirationTime ]); } elseif ($user_type === 'driver') { if ($context === 'token_change') { // Delete old verification attempts $stmtDel = $con->prepare("DELETE FROM `token_verification_driver` WHERE `phone_number` = ?"); $stmtDel->execute([$encryptedPhone]); // Insert new attempt $stmtIns = $con->prepare(" INSERT INTO `token_verification_driver` (`phone_number`, `token`, `expiration_time`, `verified`, `created_at`) VALUES (?, ?, ?, 0, NOW()) "); $stmtIns->execute([ $encryptedPhone, $encryptedOtp, $expirationTime ]); } else { // Delete old verification attempts $stmtDel = $con->prepare("DELETE FROM `phone_verification` WHERE `phone_number` = ?"); $stmtDel->execute([$encryptedPhone]); // Insert new attempt $stmtIns = $con->prepare(" INSERT INTO `phone_verification` (`phone_number`, `driverId`, `email`, `token_code`, `expiration_time`, `is_verified`, `created_at`) VALUES (?, ?, ?, ?, ?, 0, NOW()) "); $stmtIns->execute([ $encryptedPhone, $driverId ?: '', $encryptedEmail, $encryptedOtp, $expirationTime ]); } } else { if ($context === 'token_change') { // Delete old verification attempts $stmtDel = $con->prepare("DELETE FROM `token_verification` WHERE `phone_number` = ?"); $stmtDel->execute([$encryptedPhone]); // Insert new attempt $stmtIns = $con->prepare(" INSERT INTO `token_verification` (`phone_number`, `token`, `expiration_time`, `verified`, `created_at`) VALUES (?, ?, ?, 0, NOW()) "); $stmtIns->execute([ $encryptedPhone, $encryptedOtp, $expirationTime ]); } else { // Delete old verification attempts $stmtDel = $con->prepare("DELETE FROM `phone_verification_passenger` WHERE `phone_number` = ?"); $stmtDel->execute([$encryptedPhone]); // Insert new attempt $stmtIns = $con->prepare(" INSERT INTO `phone_verification_passenger` (`phone_number`, `token`, `expiration_time`, `verified`, `created_at`) VALUES (?, ?, ?, 0, NOW()) "); $stmtIns->execute([ $encryptedPhone, $encryptedOtp, $expirationTime ]); } } jsonSuccess(null, "OTP sent and saved successfully"); } catch (PDOException $e) { error_log("⚠️ [OTP DB Save] Error: " . $e->getMessage()); jsonError("OTP sent but failed to save verification data"); } } else { jsonError("Failed to send verification code. Please try again."); }