Files
tripz-llc/backend/auth/otp/request.php
T
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

229 lines
9.3 KiB
PHP

<?php
// File: backend/auth/otp/request.php
// Unified OTP request endpoint with geographical routing (Syria, Egypt, Jordan)
// Enable error reporting for debug
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
require_once __DIR__ . '/providers.php';
// 1. Rate Limiting check (max 3 requests per 5 minutes per IP)
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'otp');
// 2. Fetch input parameters
$receiver = filterRequest("receiver");
if (empty($receiver)) {
$receiver = filterRequest("phone_number");
}
$user_type = filterRequest("user_type");
// user_type is taken from request only (JWT not trusted without signature verification)
$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] Kazumi SMS failed. Falling back to Intaleq OTP WhatsApp.");
$sentSuccessfully = sendIntaleqOtp($receiver, $otp, 'whatsapp');
}
break;
case 'syria':
// Syria uses Nabeh
$sentSuccessfully = sendNabehOtp($receiver, $otp, $method ?? '', $user_type ?? 'passenger');
break;
case 'jordan':
// Jordan uses Nabeh
$sentSuccessfully = sendNabehOtp($receiver, $otp, $method ?? '', $user_type ?? 'passenger');
break;
default:
// Default fallback to Kazumi SMS
$sentSuccessfully = sendKazumiSms($receiver, $otp);
if (!$sentSuccessfully) {
error_log("⚠️ [Default OTP Failover] Kazumi SMS failed. Falling back to Nabeh OTP.");
$sentSuccessfully = sendNabehOtp($receiver, $otp, $method ?? '', $user_type ?? 'passenger');
}
break;
}
// 6. DB Storage on Success
if ($sentSuccessfully) {
$encryptedPhone = otpPhoneKey($receiver); // مفتاح بحث ثابت مستقل عن نمط التشفير
// نسخة قابلة للاسترجاع: خدمة العملاء تتابع من طلب رمزاً ولم يُكمل تسجيله،
// والمفتاح أعلاه أحادي الاتجاه فلا يُستخرج منه الرقم.
$phoneEncStored = $encryptionHelper->encryptData($receiver);
$encryptedOtp = $encryptionHelper->encryptDataGCM($otp); // Random GCM
$encryptedEmail = !empty($email) ? $encryptionHelper->encryptData($email) : '';
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`, `phone_enc`, `token_code`, `expiration_time`, `is_verified`, `created_at`)
VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE), 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$phoneEncStored,
$encryptedOtp
]);
} elseif ($user_type === 'driver') {
if ($context === 'token_change' || $context === 'payout') {
// 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 (?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE), 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$encryptedOtp
]);
} 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`, `phone_enc`, `driverId`, `email`, `token_code`, `expiration_time`, `is_verified`, `created_at`)
VALUES (?, ?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE), 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$phoneEncStored,
$driverId ?: '',
$encryptedEmail,
$encryptedOtp
]);
}
} 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`, `phone_enc`, `token`, `expiration_time`, `verified`, `created_at`)
VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE), 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$phoneEncStored,
$encryptedOtp
]);
} 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`, `phone_enc`, `token`, `expiration_time`, `verified`, `created_at`)
VALUES (?, ?, ?, DATE_ADD(NOW(), INTERVAL 5 MINUTE), 0, NOW())
");
$stmtIns->execute([
$encryptedPhone,
$phoneEncStored,
$encryptedOtp
]);
}
}
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 {
$errDetail = !empty($GLOBALS['last_otp_error']) ? $GLOBALS['last_otp_error'] : "Failed to send verification code. Please try again.";
jsonError($errDetail);
}