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>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
<?php
require_once __DIR__ . '/../../connect.php';
// لا نستقبل id أو email من التطبيق بل نأخذهم من التوكن (JWT) لزيادة الأمان
$platform = filterRequest("platform") ?: 'unknown';
$appName = filterRequest("appName") ?: 'unknown';
// الاعتماد كلياً على الـ ID المستخرج من JWT داخل connect.php
$id = $user_id;
if ($id === 'new') {
if (isset($decoded->sub) && $decoded->sub !== 'new') {
$id = $decoded->sub;
} else {
$id = filterRequest("passengerID") ?: filterRequest("passengerId");
}
}
// تجهيز الاستعلام
$sql = "SELECT
p.`id`,
p.`phone`,
p.`email`,
p.`gender`,
p.`status`,
p.`birthdate`,
p.`site`,
p.`first_name`,
p.`last_name`,
p.`sosPhone`,
p.`education`,
p.`employmentType`,
p.`maritalStatus`,
p.`created_at`,
p.`updated_at`,
phone_verification_passenger.verified,
invitesToPassengers.isInstall,
invitesToPassengers.inviteCode,
invitesToPassengers.isGiftToken,
(SELECT `version` FROM `packageInfo` WHERE platform = :platform AND appName = :appName) AS package,
promos.promo_code AS promo,
promos.amount AS discount,
promos.validity_end_date AS validity,
t.token AS fcm_token,
t.fingerPrint AS fcm_fingerprint
FROM passengers p
LEFT JOIN phone_verification_passenger
ON phone_verification_passenger.phone_number = p.phone_key
LEFT JOIN invitesToPassengers
ON invitesToPassengers.inviterPassengerPhone = p.phone
LEFT JOIN promos
ON promos.passengerID = p.id
LEFT JOIN tokens t
ON t.passengerID = p.id
WHERE p.id = :id
LIMIT 1";
// تنفيذ الاستعلام
$stmt = $con->prepare($sql);
$stmt->bindParam(':id', $id);
$stmt->bindParam(':appName', $appName);
$stmt->bindParam(':platform', $platform);
$stmt->execute();
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = $stmt->rowCount();
// تجهيز الرد
header('Content-Type: application/json');
if ($count > 0) {
foreach ($data as &$row) {
// فك تشفير الحقول الحساسة
$row['phone'] = $encryptionHelper->decryptData($row['phone']);
$row['email'] = $encryptionHelper->decryptData($row['email']);
$row['gender'] = $encryptionHelper->decryptData($row['gender']);
$row['birthdate'] = $encryptionHelper->decryptData($row['birthdate']);
$row['site'] = $encryptionHelper->decryptData($row['site']);
$row['first_name'] = $encryptionHelper->decryptData($row['first_name']);
$row['last_name'] = $encryptionHelper->decryptData($row['last_name']);
$row['sosPhone'] = $encryptionHelper->decryptData($row['sosPhone']);
$row['education'] = $encryptionHelper->decryptData($row['education']);
$row['employmentType'] = $encryptionHelper->decryptData($row['employmentType']);
$row['maritalStatus'] = $encryptionHelper->decryptData($row['maritalStatus']);
// فك تشفير توكن FCM إذا وجد
if (!empty($row['fcm_token'])) {
$row['fcm_token'] = $encryptionHelper->decryptData($row['fcm_token']);
}
}
echo json_encode([
"status" => "success",
"count" => $count,
"data" => $data
]);
} else {
error_log("User does not exist: " . $email);
echo json_encode([
"status" => "Failure",
"data" => "User does not exist."
]);
}
// تنظيف الموارد
$stmt = null;
$con = null;
exit();
@@ -0,0 +1,127 @@
<?php
// loginUsingCredentialsWithoutGooglePassenger.php
// مسار مخصص لفاحصي التطبيق (الركاب) يعمل بدون JWT Interceptors
require_once __DIR__ . '/../../core/bootstrap.php';
$email = filterRequest("email");
$password = filterRequest("password");
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
$audience = filterRequest('aud') ?: 'siro_passenger';
// 1. تطبيق حد معدل الطلبات (Rate Limiting) للفاحصين: 3 محاولات بالدقيقة لكل IP
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'tester_login');
if (!$email || !$password) {
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
exit();
}
// 2. التحقق من أن الحساب مخصص للفحص فقط (isTest check)
$allowedTesterEmailsEnv = getenv('ALLOWED_TESTER_EMAILS') ?: '';
$allowedEmails = array_filter(array_map('trim', explode(',', $allowedTesterEmailsEnv)));
if (empty($allowedEmails)) {
$allowedEmails = [
'driver_tester@siromove.com',
'passenger_tester@siromove.com',
];
}
$cleanEmail = strtolower(trim($email));
$isTester = in_array($cleanEmail, $allowedEmails) ||
substr($cleanEmail, -13) === '@siromove.com' ||
str_contains($cleanEmail, 'tester') ||
str_contains($cleanEmail, 'reviewer');
try {
$con = Database::get('main');
// تشفير الإيميل للبحث في قاعدة البيانات
$encryptedEmail = $encryptionHelper->encryptData($email);
global $blindIndex;
$emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $email) : null;
// Auto-seed/create tester passenger logic removed for security
$sql = "SELECT
p.*,
phone_verification_passenger.verified,
invitesToPassengers.isInstall,
invitesToPassengers.inviteCode,
invitesToPassengers.isGiftToken
FROM passengers p
LEFT JOIN phone_verification_passenger
ON phone_verification_passenger.phone_number = p.phone_key
LEFT JOIN invitesToPassengers
ON invitesToPassengers.inviterPassengerPhone = p.phone
WHERE p.email = :email OR (:email_bidx IS NOT NULL AND p.email_bidx = :email_bidx)
LIMIT 1";
$stmt = $con->prepare($sql);
$stmt->bindParam(':email', $encryptedEmail);
$stmt->bindParam(':email_bidx', $emailBidx);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
// فحص الباسورد
if (password_verify($password, $data['password'])) {
// التحقق من أن الحساب معلم كحساب فحص في قاعدة البيانات أو البيئة
$isTestInDb = (isset($data['is_test']) && $data['is_test'] == 1) || (isset($data['isTest']) && $data['isTest'] == 1);
if (!$isTestInDb && !$isTester) {
jsonError("Access denied. Not a tester account.");
exit();
}
// فك تشفير البيانات للرد
if(isset($data['phone'])) $data['phone'] = $encryptionHelper->decryptData($data['phone']);
if(isset($data['email'])) $data['email'] = $encryptionHelper->decryptData($data['email']);
if(isset($data['gender'])) $data['gender'] = $encryptionHelper->decryptData($data['gender']);
if(isset($data['birthdate'])) $data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
if(isset($data['site'])) $data['site'] = $encryptionHelper->decryptData($data['site']);
if(isset($data['first_name'])) $data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
if(isset($data['last_name'])) $data['last_name'] = $encryptionHelper->decryptData($data['last_name']);
if(isset($data['sosPhone'])) $data['sosPhone'] = $encryptionHelper->decryptData($data['sosPhone']);
if(isset($data['education'])) $data['education'] = $encryptionHelper->decryptData($data['education']);
if(isset($data['employmentType'])) $data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
if(isset($data['maritalStatus'])) $data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
// Force verified = 1 for the test user so the Rider app doesn't reject the login
if (isset($data['is_test']) && $data['is_test'] == 1) {
$data['verified'] = 1;
}
// توليد الـ JWT بصلاحية (tester) لتميزهم عن المستخدمين الفعليين
$jwtService = new JwtService($redis);
$jwt = $jwtService->generateAccessToken($data['id'], 'tester', $audience, $fingerprint);
echo json_encode([
"status" => "success",
"jwt" => $jwt,
"data" => [$data] // مطابق لنسق التطبيق الذي يتوقع مصفوفة
], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode([
"status" => "failure",
"message" => "Invalid credentials"
]);
}
} else {
echo json_encode([
"status" => "failure",
"message" => "Invalid credentials"
]);
}
} catch (Throwable $e) {
error_log("Error in loginUsingCredentialsWithoutGooglePassenger: " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine());
http_response_code(500);
echo json_encode([
"status" => "failure",
"message" => "Server error: " . $e->getMessage() . " in " . basename($e->getFile()) . " on line " . $e->getLine()
]);
}
exit();
+199
View File
@@ -0,0 +1,199 @@
<?php
// File: register_passenger.php
// إعدادات إظهار الأخطاء
ini_set('display_errors', 0);
error_reporting(E_ALL);
$allowRegistration = true;
require_once __DIR__ . '/../../connect.php';
// Rate Limiting: الحماية من البرمجيات الخبيثة والتسجيل العشوائي
$rateLimiter = new RateLimiter($redis);
$rateLimiter->enforce(RateLimiter::identifier(), 'register_passenger');
// تعريف بادئة للوج (Tag) لسهولة البحث عنها في ملف الأخطاء
$logTag = "[Register_Debug_passenger]";
$step = 0;
try {
// ======================================================
// Step 1: استقبال البيانات
// ======================================================
$step = 1;
$phoneNumber = filterRequest("phone_number");
$firstName = filterRequest("first_name");
$lastName = filterRequest("last_name");
$email = filterRequest("email");
// طباعة وصول البيانات (مع إخفاء جزء من الرقم)
error_log("$logTag Step 1: Received request. Phone: " . substr($phoneNumber, 0, 7) . "*****");
// ======================================================
// Step 2: التحقق من المدخلات
// ======================================================
$step = 2;
if (empty($phoneNumber) || empty($firstName) || empty($lastName)) {
error_log("$logTag Step 2 Error: Missing required fields.");
jsonError("Required fields are missing.");
exit();
}
// ======================================================
// Step 3: معالجة الإيميل
// ======================================================
$step = 3;
if (empty($email)) {
$email = $phoneNumber . '@intaleqapp.com';
error_log("$logTag Step 3: Email was empty, generated default: " . substr($email, 0, 5) . "***");
}
// ======================================================
// Step 4: تشفير البيانات
// ======================================================
$step = 4;
error_log("$logTag Step 4: Encrypting data...");
if (!isset($encryptionHelper)) {
throw new Exception("Encryption Helper class is missing.");
}
$phoneNumber_encrypted = $encryptionHelper->encryptData($phoneNumber);
$firstName_encrypted = $encryptionHelper->encryptData($firstName);
$lastName_encrypted = $encryptionHelper->encryptData($lastName);
$email_encrypted = $encryptionHelper->encryptData($email);
$uniqueId = substr(md5($phoneNumber), 0, 20);
$password_hashed = password_hash($email . $uniqueId, PASSWORD_DEFAULT);
$unknown_encrypted = $encryptionHelper->encryptData("unknown yet");
// ======================================================
// Step 4.5: التحقق الفعلي من ملكية رقم الهاتف (🔥 Fix)
// ======================================================
// كانت هذه النقطة تسمح بإنشاء حساب راكب بأي رقم هاتف بدون إثبات
// ملكيته فعلياً — auth/otp/verify.php يُعلّم الصف verified=1 لكن
// register_passenger.php لم يكن يتحقق من ذلك إطلاقاً. الآن نشترط
// وجود صف تحقق ناجح (verified=1) لنفس رقم الهاتف خلال آخر 30 دقيقة
// (مهلة أوسع من صلاحية الرمز نفسه [5 دقائق] لإعطاء وقت كافٍ لإكمال
// نموذج التسجيل بعد التحقق مباشرة).
$step = 4.5;
$verifyCheckStmt = $con->prepare(
"SELECT id FROM phone_verification_passenger
WHERE phone_number = ? AND verified = 1 AND created_at > DATE_SUB(NOW(), INTERVAL 30 MINUTE)
LIMIT 1"
);
$verifyCheckStmt->execute([$phoneNumber_encrypted]);
if ($verifyCheckStmt->rowCount() === 0) {
error_log("$logTag Step 4.5 Error: Phone number not verified via OTP.");
jsonError("Phone number must be verified before registration.");
exit();
}
// ======================================================
// Step 5: إنشاء ID فريد
// ======================================================
$step = 5;
// $uniqueId = substr(md5(uniqid(mt_rand(), true)), 0, 20);
// $uniqueId is now generated earlier
error_log("$logTag Step 5: Generated Unique ID: $uniqueId");
// ======================================================
// Step 6: التحقق من وجود المستخدم (Database Check)
// ======================================================
$step = 6;
// كشف التكرار عبر الفهرس الأعمى + المقارنة القديمة: بدون الفهرس يفشل
// الكشف بعد الانتقال إلى GCM فيُسمح بتسجيل نفس الرقم مرتين.
global $blindIndex;
$phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phoneNumber) : null;
$emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $email) : null;
$nameBidx = $blindIndex ? $blindIndex->index('passengers.name', trim("$firstName $lastName")) : null;
// مفتاح ربط جداول التحقق — يجب أن يطابق otpPhoneKey() حرفياً
$phoneKey = otpPhoneKey($phoneNumber);
$checkStmt = $con->prepare(
"SELECT id FROM passengers WHERE phone = ? OR (? IS NOT NULL AND phone_bidx = ?)"
);
$checkStmt->execute([$phoneNumber_encrypted, $phoneBidx, $phoneBidx]);
if ($checkStmt->rowCount() > 0) {
error_log("$logTag Step 6 Error: User already exists.");
jsonError("User with this phone number or email already exists.");
exit();
}
// ======================================================
// Step 7: الإضافة (Insert User)
// ======================================================
$step = 7;
error_log("$logTag Step 7: Inserting into passengers table...");
$insertStmt = $con->prepare("
INSERT INTO passengers (id, first_name, last_name, email, phone, password, gender, birthdate, site, sosPhone, education, employmentType, maritalStatus, status, created_at, updated_at, phone_bidx, email_bidx, name_bidx, phone_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', NOW(), NOW(), ?, ?, ?, ?)
");
$success = $insertStmt->execute([
$uniqueId,
$firstName_encrypted,
$lastName_encrypted,
$email_encrypted,
$phoneNumber_encrypted,
$password_hashed,
$unknown_encrypted,
$unknown_encrypted,
$unknown_encrypted,
$unknown_encrypted,
$unknown_encrypted,
$unknown_encrypted,
$unknown_encrypted,
// فهارس البحث: تُكتب مع السجل حتى يكون قابلاً للبحث فوراً
$phoneBidx,
$emailBidx,
$nameBidx,
$phoneKey
]);
if (!$success) {
$errorInfo = $insertStmt->errorInfo();
// طباعة تفاصيل خطأ الـ SQL في اللوج
error_log("$logTag Step 7 Error: SQL Insert Failed. Details: " . json_encode($errorInfo));
jsonError("Failed to create user account.");
exit();
}
// ======================================================
// Step 9: جلب البيانات لإعادتها
// ======================================================
$step = 9;
$userStmt = $con->prepare("SELECT * FROM passengers WHERE id = ?");
$userStmt->execute([$uniqueId]);
$newUser = $userStmt->fetch(PDO::FETCH_ASSOC);
// ======================================================
// Step 10: فك التشفير وإرسال الرد
// ======================================================
$step = 10;
if ($newUser) {
unset($newUser['password']);
foreach ($newUser as $key => &$value) {
if ($key !== 'id' && $key !== 'status' && $key !== 'created_at' && $key !== 'updated_at' && !is_null($value)) {
$value = $encryptionHelper->decryptData($value);
}
}
}
error_log("$logTag Success: User registered successfully.");
jsonSuccess(["status" => "registration_success", "data" => $newUser]);
} catch (PDOException $e) {
// طباعة خطأ قاعدة البيانات في اللوج
error_log("$logTag PDO Exception at Step $step: " . $e->getMessage());
jsonError("Database Error.");
} catch (Exception $e) {
// طباعة الأخطاء العامة في اللوج
error_log("$logTag General Exception at Step $step: " . $e->getMessage());
jsonError("General Error.");
}
?>
+30
View File
@@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../../connect.php';
$email = filterRequest("email");
$headers = "MIME-Version: 1.0" . "\r\n";
$headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
$headers .= "From: SEFER Team" . "\r\n";
// Create the email subject and body
$subject = 'Your SEFER account has been deleted';
$body = '
Dear passenger,
We are sorry to see you go, but we respect your decision to delete your SEFER account.
We would like to thank you for using our platform and for being a part of the SEFER community. We hope that you had a positive experience and that we were able to make your travels easier and more enjoyable.
If you have any questions or concerns, please do not hesitate to contact us.
Sincerely,
The SEFER Team
';
// Send the email
mail($email, $subject, $body);
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
require_once __DIR__ . '/../../connect.php';
// Ensure the caller has a valid user_id
if (empty($user_id)) {
jsonError("Unauthorized", 401);
exit;
}
$latitude = filterRequest("latitude");
$longitude = filterRequest("longitude");
// Validate inputs
if ($latitude === '' || $longitude === '') {
jsonError("Latitude and longitude are required", 400);
exit;
}
try {
// Insert location log
$sql = "INSERT INTO `passenger_opening_locations` (`passenger_id`, `latitude`, `longitude`)
VALUES (:passenger_id, :latitude, :longitude)";
$stmt = $con->prepare($sql);
$stmt->bindParam(':passenger_id', $user_id);
$stmt->bindParam(':latitude', $latitude);
$stmt->bindParam(':longitude', $longitude);
if ($stmt->execute()) {
jsonSuccess(null, "Location logged successfully");
} else {
jsonError("Failed to log location", 500);
}
} catch (PDOException $e) {
error_log("Database Error in save_passenger_location.php: " . $e->getMessage());
jsonError("An error occurred while logging location", 500);
}
?>
+88
View File
@@ -0,0 +1,88 @@
<?php
// File: send_otp.php (بديل عن النسخة المعتمدة على RaseelPlus)
require_once __DIR__ . '/../../connect.php';
/* 1) توليد رمز التحقق (3 خانات) */
$otp = (string)random_int(100, 999);
$receiver = filterRequest("receiver");
if (empty($receiver)) {
jsonError('Phone number is required.');
exit();
}
/* 2) إرسال عبر بوابة الفلاش كول / واتساب */
$nabehUrl = 'https://otp.intaleqapp.com/api/request-otp.php';
$appKey = getenv('NABEH_OTP_APP_KEY');
$payload = [
'phone' => $receiver,
'device_type' => 'android',
'method' => 'whatsapp',
'code' => $otp
];
$ch = curl_init($nabehUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-App-Key: $appKey"
],
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 5
]);
$res = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("⚠️ [Flash Call OTP Token Passenger] Curl Error: $error");
jsonError('Failed to connect to OTP service');
exit;
}
$decoded = json_decode((string)$res, true);
$sentOK = ($httpCode === 200 && ($decoded['success'] ?? false));
if ($sentOK) {
/* 3) حفظ الرمز في Redis + قاعدة البيانات */
$receiver_enc = otpPhoneKey($receiver);
$otp_enc = otpPhoneKey($otp); // يجب أن يطابق صيغة المقارنة في verify_otp
$exp = date('Y-m-d H:i:s', strtotime('+5 minutes'));
$now = date('Y-m-d H:i:s');
try {
// Save to MySQL
$con->prepare("DELETE FROM token_verification WHERE phone_number = ?")
->execute([$receiver_enc]);
$stmt = $con->prepare("
INSERT INTO token_verification
(phone_number, token, expiration_time, verified, created_at)
VALUES (?, ?, ?, 0, ?)
");
$stmt->execute([$receiver_enc, $otp_enc, $exp, $now]);
// Also save to Redis for verify_otp.php compatibility
if ($redis) {
$redis->setex("otp:passenger:$receiver", 300, $otp);
}
jsonSuccess(null, 'OTP sent and saved successfully');
} catch (PDOException $e) {
error_log("[send_otp.php] " . $e->getMessage());
jsonError('OTP sent but failed to save to database');
}
} else {
$errMsg = $decoded['message'] ?? 'Unknown error';
jsonError('Failed to send OTP');
}
?>
@@ -0,0 +1,81 @@
<?php
// File: verify_otp.php (with enhanced logging)
// siro_v1/auth/token_passenger
require_once __DIR__ . '/../../connect.php';
// --- Start of Script Execution ---
error_log("--- [verify_otp.php] Script execution started. ---");
$phoneNumber = filterRequest("phone_number");
$otp = filterRequest("otp");
// Log received data for debugging. Be mindful of logging sensitive data in production.
error_log("[verify_otp.php] Received phone_number: $phoneNumber | Received otp: $otp");
if (empty($phoneNumber) || empty($otp)) {
error_log("[verify_otp.php] Error: Phone number or OTP is empty.");
jsonError("Phone number and OTP are required.");
exit();
}
$phoneNumber_encrypted = otpPhoneKey($phoneNumber);
// الرمز يُقارن بالتساوي أيضاً، فيحتاج نفس الصيغة الثابتة
$otp_encrypted = otpPhoneKey($otp);
try {
// 1. التحقق من Redis بدلاً من MySQL
if (!$redis) {
jsonError("Security service unavailable");
exit;
}
$cachedOtp = $redis->get("otp:passenger:$phoneNumber");
if ($cachedOtp && $cachedOtp === $otp) {
// ننجح في التحقق ونحذف المفتاح من Redis لمنع استخدامه مرة أخرى (One-time use)
$redis->del("otp:passenger:$phoneNumber");
error_log("[verify_otp.php] OTP verified via Redis for phone: $phoneNumber");
// 2. التحقق من وجود الراكب في قاعدة البيانات
$passengerStmt = $con->prepare("SELECT id FROM passengers WHERE phone = ?");
$passengerStmt->execute([$phoneNumber_encrypted]);
$passenger = $passengerStmt->fetch(PDO::FETCH_ASSOC);
if ($passenger) {
$passengerID = $passenger['id'];
// تحديث التوكن والبصمة إن وجدا
$newToken = filterRequest("token");
$fingerPrint = filterRequest("fingerPrint");
if ($newToken && $fingerPrint) {
$tokenEncrypted = $encryptionHelper->encryptData($newToken);
$updateTokenStmt = $con->prepare("UPDATE tokens SET token = ?, fingerPrint = ? WHERE passengerID = ?");
$updateTokenStmt->execute([$tokenEncrypted, $fingerPrint, $passengerID]);
}
printSuccess([
"message" => "Token verified and updated.",
"isRegistered" => true,
"passengerID" => $passengerID
]);
} else {
printSuccess([
"message" => "Phone verified, passenger not found.",
"isRegistered" => false
]);
}
} else {
error_log("[verify_otp.php] Invalid or expired OTP for phone: $phoneNumber");
jsonError("Invalid or expired OTP.");
}
} catch (Exception $e) {
// Log the detailed database error message for debugging.
error_log("[verify_otp.php] FATAL DATABASE ERROR: " . $e->getMessage());
jsonError("Database error");
}
?>