قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
300 lines
9.8 KiB
PHP
300 lines
9.8 KiB
PHP
<?php
|
||
// File: backend/auth/otp/providers.php
|
||
// Encapsulates external OTP gateway API calls for Kazumi, Intaleq, and Nabeh.
|
||
|
||
/**
|
||
* Send SMS OTP via Kazumi SMS Gateway (Egypt)
|
||
*
|
||
* @param string $receiver Recipient phone number (e.g. +2010xxxxxxxx)
|
||
* @param string $otp 3-digit verification code
|
||
* @return bool True if OTP was sent successfully
|
||
*/
|
||
function sendKazumiSms(string $receiver, string $otp): bool {
|
||
$username = getenv('SMS_USERNAME');
|
||
$password = getenv('SMS_PASSWORD_EGYPT');
|
||
$sender = getenv('SMS_SENDER');
|
||
|
||
if (!$username || !$password || !$sender) {
|
||
error_log("⚠️ [Kazumi OTP] Missing credentials in environment variables.");
|
||
return false;
|
||
}
|
||
|
||
$message = "Siro app code is " . $otp;
|
||
$apiUrl = 'https://sms.kazumi.me/api/sms/send-sms';
|
||
|
||
$payload = [
|
||
'username' => $username,
|
||
'password' => $password,
|
||
'language' => 'e',
|
||
'sender' => $sender,
|
||
'receiver' => $receiver,
|
||
'message' => $message
|
||
];
|
||
|
||
$response = curlCall("POST", $apiUrl, json_encode($payload), [
|
||
"Content-Type: application/json"
|
||
]);
|
||
|
||
if ($response) {
|
||
$decoded = json_decode($response, true);
|
||
if (isset($decoded['message']) && $decoded['message'] === 'Success') {
|
||
return true;
|
||
}
|
||
error_log("❌ [Kazumi OTP] API returned failure response: " . $response);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Retrieve Nabeh JWT Bearer Token, caching it in Redis for 24 hours.
|
||
*
|
||
* @return string|null The Bearer token, or null on failure.
|
||
*/
|
||
function getNabehBearerToken(): ?string {
|
||
global $redis;
|
||
|
||
// 1. Try fetching from Redis first
|
||
if ($redis) {
|
||
try {
|
||
$cachedToken = $redis->get('nabeh_bearer_token');
|
||
if ($cachedToken) {
|
||
return $cachedToken;
|
||
}
|
||
} catch (Exception $e) {
|
||
$msg = "⚠️ [Nabeh Auth Redis] Error reading token: " . $e->getMessage();
|
||
error_log($msg);
|
||
}
|
||
}
|
||
|
||
// 2. Token not cached, authenticate via Nabeh Login API
|
||
$email = getenv('NABEH_EMAIL');
|
||
$password = getenv('NABEH_PASSWORD');
|
||
|
||
if (!$email || !$password) {
|
||
$msg = "⚠️ [Nabeh Auth] Missing NABEH_EMAIL or NABEH_PASSWORD environment variables.";
|
||
$GLOBALS['last_otp_error'] = $msg;
|
||
error_log($msg);
|
||
return null;
|
||
}
|
||
|
||
$apiUrl = 'https://nabeh.intaleqapp.com/api/auth/login';
|
||
$payload = [
|
||
'email' => $email,
|
||
'password' => $password
|
||
];
|
||
|
||
$response = curlCall("POST", $apiUrl, json_encode($payload), [
|
||
'Content-Type: application/json'
|
||
]);
|
||
|
||
$debugLog = "[Nabeh Auth Debug] Request: $apiUrl | Response: $response";
|
||
error_log($debugLog);
|
||
|
||
if ($response) {
|
||
$decoded = json_decode($response, true);
|
||
$token = $decoded['token'] ?? $decoded['message']['token'] ?? $decoded['jwt'] ?? $decoded['access_token'] ?? null;
|
||
if ($token) {
|
||
|
||
// 3. Cache token in Redis for 24h
|
||
if ($redis) {
|
||
try {
|
||
$redis->setex('nabeh_bearer_token', 86400, $token);
|
||
error_log("[Nabeh Auth Debug] Token cached in Redis successfully.");
|
||
} catch (Exception $e) {
|
||
$msg = "⚠️ [Nabeh Auth Redis Cache Save] Error saving token: " . $e->getMessage();
|
||
error_log($msg);
|
||
}
|
||
}
|
||
return $token;
|
||
}
|
||
$msg = "❌ [Nabeh Auth Login Failed] Response: " . $response;
|
||
$GLOBALS['last_otp_error'] = $msg;
|
||
error_log($msg);
|
||
} else {
|
||
$msg = "❌ [Nabeh Auth Login Failed] Empty response from login API.";
|
||
$GLOBALS['last_otp_error'] = $msg;
|
||
error_log($msg);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Send OTP via Nabeh JWT Auth Gateway (WhatsApp, Voice, etc.)
|
||
*
|
||
* @param string $receiver Recipient phone number
|
||
* @param string $otp 3-digit verification code
|
||
* @param string $method text | voice | image | whatsapp
|
||
* @param string $user_type passenger | driver | admin | service
|
||
* @return bool True if OTP was sent successfully
|
||
*/
|
||
function sendNabehOtp(string $receiver, string $otp, string $method = '', string $user_type = 'passenger'): bool {
|
||
$bearerToken = getNabehBearerToken();
|
||
if (!$bearerToken) {
|
||
if (empty($GLOBALS['last_otp_error'])) {
|
||
$GLOBALS['last_otp_error'] = "⚠️ [Nabeh OTP] Failed to obtain dynamic JWT Bearer token.";
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Strip symbols for Nabeh endpoint
|
||
$phoneRaw = preg_replace('/\D+/', '', $receiver);
|
||
|
||
// Map method/type (Image OTP card is default for Nabeh)
|
||
$type = ($method === 'text') ? 'text' : (($method === 'voice') ? 'voice' : 'image');
|
||
|
||
$appName = 'سيرو رايدر';
|
||
if ($user_type === 'driver') {
|
||
$appName = 'سيرو درايفر';
|
||
} elseif ($user_type === 'admin') {
|
||
$appName = 'سيرو الأدمن';
|
||
} elseif ($user_type === 'service') {
|
||
$appName = 'سيرو للخدمات';
|
||
}
|
||
|
||
// First attempt with the chosen type
|
||
$result = _nabehOtpAttempt($phoneRaw, $type, $otp, $appName, $bearerToken);
|
||
if ($result) {
|
||
return true;
|
||
}
|
||
|
||
// Fallback: if image failed, retry with text
|
||
if ($type === 'image') {
|
||
error_log("ℹ️ [Nabeh OTP Fallback] Image failed, retrying with text type...");
|
||
$result = _nabehOtpAttempt($phoneRaw, 'text', $otp, $appName, $bearerToken);
|
||
if ($result) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Internal helper: single OTP send attempt to Nabeh
|
||
*/
|
||
function _nabehOtpAttempt(string $phone, string $type, string $otp, string $appName, string $bearerToken): bool {
|
||
$apiUrl = 'https://nabeh.intaleqapp.com/api/otp/send';
|
||
$payload = [
|
||
'phone' => $phone,
|
||
'type' => $type,
|
||
'code' => $otp,
|
||
'message' => "رمز التحقق الخاص بك لتطبيق {$appName} هو: *{code}* \n الرجاء عدم مشاركته مع أي شخص."
|
||
];
|
||
|
||
$response = curlCall("POST", $apiUrl, json_encode($payload), [
|
||
'Content-Type: application/json',
|
||
"Authorization: Bearer $bearerToken"
|
||
]);
|
||
|
||
if ($response) {
|
||
$decoded = json_decode($response, true);
|
||
error_log("ℹ️ [Nabeh OTP Response type=$type] " . $response);
|
||
if ($decoded) {
|
||
$statusStr = strtolower((string)($decoded['status'] ?? ''));
|
||
$msgStr = strtolower((string)($decoded['message'] ?? ''));
|
||
$errStr = strtolower((string)($decoded['error'] ?? ''));
|
||
if (
|
||
!empty($decoded['success']) ||
|
||
in_array($statusStr, ['success', 'ok', 'true', '200', 'sent', 'queued', '1'], true) ||
|
||
($decoded['status'] ?? false) === true ||
|
||
($decoded['code'] ?? 0) === 200 ||
|
||
!empty($decoded['message_id']) ||
|
||
!empty($decoded['id']) ||
|
||
!empty($decoded['token']) ||
|
||
strpos($msgStr, 'success') !== false ||
|
||
strpos($msgStr, 'sent') !== false ||
|
||
strpos($msgStr, 'تم') !== false ||
|
||
strpos($errStr, 'via gateway') !== false
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
$msg = "❌ [Nabeh OTP type=$type] Response: " . $response;
|
||
$GLOBALS['last_otp_error'] = $msg;
|
||
error_log($msg);
|
||
} else {
|
||
$msg = "❌ [Nabeh OTP type=$type] Empty cURL response.";
|
||
$GLOBALS['last_otp_error'] = $msg;
|
||
error_log($msg);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Send OTP via Intaleq Static OTP Gateway (using body app_key parameter)
|
||
*
|
||
* @param string $receiver Recipient phone number
|
||
* @param string $otp 3-digit verification code
|
||
* @param string $method whatsapp | sms | voice | flash_call
|
||
* @return bool True if OTP was sent successfully
|
||
*/
|
||
function sendIntaleqOtp(string $receiver, string &$otp, string $method = 'whatsapp'): bool {
|
||
$appKey = getenv('NABEH_OTP_APP_KEY');
|
||
|
||
if (!$appKey) {
|
||
error_log("⚠️ [Intaleq OTP] Missing NABEH_OTP_APP_KEY in environment.");
|
||
return false;
|
||
}
|
||
|
||
// Normalize receiver to start with +
|
||
$phoneWithPlus = (strpos($receiver, '+') === 0) ? $receiver : '+' . $receiver;
|
||
|
||
$apiUrl = 'https://otp.intaleqapp.com/api/request-otp.php';
|
||
$payload = [
|
||
'phone' => $phoneWithPlus,
|
||
'app_key' => $appKey
|
||
];
|
||
|
||
$response = curlCall("POST", $apiUrl, json_encode($payload), [
|
||
'Content-Type: application/json'
|
||
]);
|
||
|
||
if ($response) {
|
||
$decoded = json_decode($response, true);
|
||
if ($decoded && (!empty($decoded['success']) || ($decoded['status'] ?? '') === 'success')) {
|
||
if (isset($decoded['otp'])) {
|
||
$otp = (string)$decoded['otp'];
|
||
}
|
||
return true;
|
||
}
|
||
$msg = "❌ [Intaleq OTP] API returned failure response: " . $response;
|
||
error_log($msg);
|
||
} else {
|
||
error_log("❌ [Intaleq OTP] Empty response or cURL failed.");
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Generic cURL execution helper
|
||
*/
|
||
function curlCall(string $method, string $url, string $data, array $headers): ?string {
|
||
$ch = curl_init($url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_CUSTOMREQUEST => $method,
|
||
CURLOPT_POSTFIELDS => $data,
|
||
CURLOPT_HTTPHEADER => $headers,
|
||
CURLOPT_TIMEOUT => 35,
|
||
CURLOPT_CONNECTTIMEOUT => 10
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$error = curl_error($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($error) {
|
||
$msg = "⚠️ [OTP cURL] Error calling $url: $error";
|
||
error_log($msg);
|
||
return null;
|
||
}
|
||
|
||
if ($httpCode !== 200) {
|
||
$msg = "⚠️ [OTP cURL] Non-200 HTTP code $httpCode from $url. Response: $response";
|
||
error_log($msg);
|
||
}
|
||
|
||
return $response;
|
||
}
|