Files
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 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>
2026-07-27 05:14:13 +03:00

300 lines
9.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}