false, 'message' => 'method_not_allowed']); exit; } require_once __DIR__ . '/../includes/Database.php'; 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'); // Parse request body $input = json_decode(file_get_contents('php://input'), true); if (!$input || !isset($input['phone'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'missing_phone']); RequestLogger::log('request-otp', 'POST', $input, 400, 'missing_phone'); exit; } $phone = trim($input['phone']); $deviceType = isset($input['device_type']) ? strtolower(trim($input['device_type'])) : 'android'; // Validate device_type if (!in_array($deviceType, ['android', 'ios'], true)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid_device_type']); RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_device_type'); exit; } // Validate phone format (E.164) if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) { http_response_code(400); echo json_encode([ 'success' => false, 'message' => 'invalid_phone_format', 'hint' => 'Phone must be in E.164 format, e.g. +9627XXXXXXXX', ]); RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_phone_format'); exit; } // Rate limit check: max 3 requests per phone per 10 minutes $rateLimit = new RateLimit(); if (!$rateLimit->check("otp:{$phone}")) { $remaining = $rateLimit->remaining("otp:{$phone}"); $ttl = $rateLimit->ttl("otp:{$phone}"); http_response_code(429); echo json_encode([ 'success' => false, 'message' => 'rate_limit_exceeded', 'retry_after' => $ttl, 'remaining' => $remaining, ]); RequestLogger::log('request-otp', 'POST', $input, 429, 'rate_limit_exceeded'); exit; } // IP-based rate limiting $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; if (!$rateLimit->checkIp($clientIp, 'request-otp', 30, 60)) { http_response_code(429); echo json_encode(['success' => false, 'message' => 'ip_rate_limit_exceeded']); RequestLogger::log('request-otp', 'POST', $input, 429, 'ip_rate_limit'); exit; } // Generate 4-digit OTP (cryptographically secure) $otpCode = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT); // Determine delivery method $method = 'flash_call'; // Default fallback $methodInput = isset($input['method']) ? strtolower(trim($input['method'])) : null; if ($methodInput && in_array($methodInput, ['flash_call', 'sms', 'whatsapp', 'telegram'], true)) { $method = $methodInput; } else { $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(); try { $db->beginTransaction(); if ($method === 'flash_call') { // Find available caller device (round-robin) $stmt = $db->prepare( "SELECT device_id, phone_number, sim_slot FROM caller_devices WHERE is_active = 1 ORDER BY calls_today ASC, last_seen DESC LIMIT 1" ); $stmt->execute(); $device = $stmt->fetch(); if (!$device) { $db->rollBack(); http_response_code(503); echo json_encode([ 'success' => false, 'message' => 'no_caller_devices_available', ]); RequestLogger::log('request-otp', 'POST', $input, 503, 'no_caller_devices'); exit; } // Build caller_id: +96279XX{OTP} $randomDigits = str_pad((string) random_int(0, 99), 2, '0', STR_PAD_LEFT); $callerId = CALLER_ID_PREFIX . $randomDigits . $otpCode; // Insert OTP request $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, device_id, method, expires_at) 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); $stmt = $db->prepare( "INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at) VALUES (?, ?, '', 'pending_sms', 'sms', ?)" ); $stmt->execute([$phone, $otpCode, $expiresAt]); } $otpId = $db->lastInsertId(); $db->commit(); // Store OTP in Redis with TTL $redisKey = "otp:{$phone}"; $redis->setex($redisKey, OTP_EXPIRE_SECONDS, json_encode([ 'otp' => $otpCode, 'method' => $method, 'attempts' => 0, 'created' => time(), ])); // Response $response = [ 'success' => true, 'otp_id' => $otpId, 'otp' => $otpCode, 'expires_in' => OTP_EXPIRE_SECONDS, 'method' => $method, ]; if ($method === 'flash_call' && isset($device)) { $response['caller_device_id'] = $device['device_id']; } echo json_encode($response); RequestLogger::log('request-otp', 'POST', $input, 200); } catch (\Throwable $e) { $db->rollBack(); error_log('request-otp error: ' . $e->getMessage()); http_response_code(500); echo json_encode(['success' => false, 'message' => 'internal_error']); RequestLogger::log('request-otp', 'POST', $input, 500, $e->getMessage()); }