تحديث إعدادات بوابة الواتساب لتكون مستقلة ومحمية
This commit is contained in:
@@ -34,6 +34,7 @@ require_once __DIR__ . '/../includes/Redis.php';
|
||||
require_once __DIR__ . '/../includes/RateLimit.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Logger.php';
|
||||
require_once __DIR__ . '/../includes/WhatsApp.php';
|
||||
|
||||
// Authenticate — requires app key (Flutter app)
|
||||
Auth::requireAuth('app');
|
||||
@@ -100,7 +101,20 @@ if (!$rateLimit->checkIp($clientIp, 'request-otp', 30, 60)) {
|
||||
$otpCode = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
|
||||
// Determine delivery method
|
||||
$method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
|
||||
$method = 'flash_call'; // Default fallback
|
||||
$whatsappAvailable = false;
|
||||
|
||||
try {
|
||||
$whatsappAvailable = WhatsAppClient::isAvailable($phone);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('WhatsApp check failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($whatsappAvailable) {
|
||||
$method = 'whatsapp';
|
||||
} else {
|
||||
$method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
$redis = RedisClient::getInstance();
|
||||
@@ -142,6 +156,43 @@ try {
|
||||
VALUES (?, ?, ?, 'pending', ?, 'flash_call', ?)"
|
||||
);
|
||||
$stmt->execute([$phone, $otpCode, $callerId, $device['device_id'], $expiresAt]);
|
||||
} else if ($method === 'whatsapp') {
|
||||
// WhatsApp delivery
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at)
|
||||
VALUES (?, ?, '', 'pending_whatsapp', 'whatsapp', ?)"
|
||||
);
|
||||
$stmt->execute([$phone, $otpCode, $expiresAt]);
|
||||
$otpId = $db->lastInsertId();
|
||||
|
||||
// Try to generate premium dynamic base64 OTP image
|
||||
$imagePngBase64 = null;
|
||||
try {
|
||||
$imagePngBase64 = WhatsAppClient::generateOtpImageBase64($otpCode);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('Failed to generate OTP image: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Message caption / body
|
||||
$messageText = "رمز التحقق الخاص بك هو: " . $otpCode . "\nيرجى إدخاله في التطبيق لإكمال العملية.";
|
||||
|
||||
$sent = false;
|
||||
try {
|
||||
if ($imagePngBase64) {
|
||||
// Send premium image message with caption
|
||||
$sent = WhatsAppClient::sendMessage($phone, "رمز التحقق الخاص بك هو: " . $otpCode, $imagePngBase64);
|
||||
} else {
|
||||
// Fallback to text message
|
||||
$sent = WhatsAppClient::sendMessage($phone, $messageText);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('WhatsApp sendMessage error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if (!$sent) {
|
||||
throw new \Exception('Failed to send OTP via WhatsApp');
|
||||
}
|
||||
} else {
|
||||
// SMS delivery — no specific caller_id needed for the OTP request
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
||||
|
||||
@@ -39,5 +39,10 @@ define('CALLER_ID_PREFIX', '+96279');
|
||||
define('LOG_REQUESTS', true);
|
||||
define('LOG_FILE', __DIR__ . '/logs/api.log');
|
||||
|
||||
// WhatsApp Gateway Configuration
|
||||
define('WHATSAPP_GATEWAY_URL', getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3732');
|
||||
define('WHATSAPP_WEBHOOK_SECRET', getenv('WHATSAPP_WEBHOOK_SECRET') ?: 'flash_call_otp_webhook_secret_key');
|
||||
define('WHATSAPP_SESSION_KEY', getenv('WHATSAPP_SESSION_KEY') ?: 'flash_call_otp');
|
||||
|
||||
// Timezone
|
||||
date_default_timezone_set('Asia/Amman');
|
||||
|
||||
@@ -15,8 +15,8 @@ CREATE TABLE IF NOT EXISTS otp_requests (
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
otp_code VARCHAR(6) NOT NULL,
|
||||
caller_id VARCHAR(20) NOT NULL DEFAULT '',
|
||||
status ENUM('pending','pending_sms','calling','completed','failed','expired','verified') NOT NULL DEFAULT 'pending',
|
||||
method ENUM('flash_call','sms') NOT NULL DEFAULT 'flash_call',
|
||||
status ENUM('pending','pending_sms','pending_whatsapp','calling','completed','failed','expired','verified') NOT NULL DEFAULT 'pending',
|
||||
method ENUM('flash_call','sms','whatsapp','telegram') NOT NULL DEFAULT 'flash_call',
|
||||
device_id VARCHAR(50) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
158
backend/includes/WhatsApp.php
Normal file
158
backend/includes/WhatsApp.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
/**
|
||||
* WhatsApp Gateway Client Helper
|
||||
*/
|
||||
|
||||
class WhatsAppClient {
|
||||
private static $gatewayUrl;
|
||||
private static $secret;
|
||||
private static $sessionKey;
|
||||
|
||||
private static function init() {
|
||||
if (!self::$gatewayUrl) {
|
||||
self::$gatewayUrl = rtrim(defined('WHATSAPP_GATEWAY_URL') ? WHATSAPP_GATEWAY_URL : 'http://localhost:3732', '/');
|
||||
self::$secret = defined('WHATSAPP_WEBHOOK_SECRET') ? WHATSAPP_WEBHOOK_SECRET : '';
|
||||
self::$sessionKey = defined('WHATSAPP_SESSION_KEY') ? WHATSAPP_SESSION_KEY : 'otp_session';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a phone number is registered on WhatsApp.
|
||||
*
|
||||
* @param string $phone Phone number in E.164 format (e.g. +96279XXXXXXXX)
|
||||
* @return bool True if registered, false otherwise
|
||||
*/
|
||||
public static function isAvailable($phone) {
|
||||
self::init();
|
||||
|
||||
$cleanPhone = preg_replace('/[^\d]/', '', $phone); // Strip '+' and other non-digits
|
||||
|
||||
$payload = json_encode([
|
||||
'session_key' => self::$sessionKey,
|
||||
'phone' => $cleanPhone
|
||||
]);
|
||||
|
||||
$ch = curl_init(self::$gatewayUrl . '/api/contacts/check');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'X-Webhook-Secret: ' . self::$secret
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200 && $response) {
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['status']) && $data['status'] === 'success') {
|
||||
return isset($data['data']['exists']) && $data['data']['exists'] === true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send OTP message via WhatsApp Gateway.
|
||||
*
|
||||
* @param string $phone Destination phone number
|
||||
* @param string $message Message text
|
||||
* @param string|null $imageBase64 Optional base64 image data
|
||||
* @return bool True if successfully sent
|
||||
*/
|
||||
public static function sendMessage($phone, $message, $imageBase64 = null) {
|
||||
self::init();
|
||||
|
||||
$cleanPhone = preg_replace('/[^\d]/', '', $phone);
|
||||
|
||||
$payloadData = [
|
||||
'session_key' => self::$sessionKey,
|
||||
'phone' => $cleanPhone,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
if ($imageBase64) {
|
||||
$payloadData['image'] = $imageBase64;
|
||||
}
|
||||
|
||||
$payload = json_encode($payloadData);
|
||||
|
||||
$ch = curl_init(self::$gatewayUrl . '/api/messages/send');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'X-Webhook-Secret: ' . self::$secret
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200 && $response) {
|
||||
$data = json_decode($response, true);
|
||||
return isset($data['status']) && $data['status'] === 'success';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate dynamic base64-encoded image containing OTP code using GD library.
|
||||
*
|
||||
* @param string $otp 4-digit OTP code
|
||||
* @return string Base64 encoded PNG image
|
||||
*/
|
||||
public static function generateOtpImageBase64($otp) {
|
||||
// Create a 300x100 image
|
||||
$im = imagecreatetruecolor(300, 100);
|
||||
if (!$im) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Colors
|
||||
$bgColor = imagecolorallocate($im, 240, 244, 248); // Soft grey-blue
|
||||
$textColor = imagecolorallocate($im, 33, 37, 41); // Dark charcoal
|
||||
$accentColor = imagecolorallocate($im, 13, 110, 253); // Premium blue
|
||||
$noiseColor = imagecolorallocate($im, 200, 210, 220); // Light noise
|
||||
|
||||
// Fill background
|
||||
imagefill($im, 0, 0, $bgColor);
|
||||
|
||||
// Draw some obfuscation lines / background noise
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
imageline($im, random_int(0, 300), random_int(0, 100), random_int(0, 300), random_int(0, 100), $noiseColor);
|
||||
}
|
||||
|
||||
// Draw some random dots
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
imagesetpixel($im, random_int(0, 300), random_int(0, 100), $noiseColor);
|
||||
}
|
||||
|
||||
// Header text (smaller)
|
||||
imagestring($im, 3, 20, 15, "Verification Code:", $textColor);
|
||||
|
||||
// Large OTP text (using larger font index 5 or custom size if possible)
|
||||
// Split OTP and draw characters with varying Y positions and styling to make OCR harder
|
||||
$chars = str_split($otp);
|
||||
$x = 90;
|
||||
foreach ($chars as $char) {
|
||||
$y = random_int(35, 45);
|
||||
imagestring($im, 5, $x, $y, $char, $accentColor);
|
||||
$x += 30;
|
||||
}
|
||||
|
||||
// Draw a bounding border
|
||||
imagerectangle($im, 0, 0, 299, 99, $noiseColor);
|
||||
|
||||
// Capture output
|
||||
ob_start();
|
||||
imagepng($im);
|
||||
$imageData = ob_get_clean();
|
||||
imagedestroy($im);
|
||||
|
||||
return base64_encode($imageData);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user