chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق». نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا `cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules · .dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore. هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ. ⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driverId = filterRequest("driver_id");
|
||||
$type = filterRequest("type");
|
||||
|
||||
// 🔒 Validate image
|
||||
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
|
||||
error_log("Upload error: Image not provided or upload failed.");
|
||||
jsonError("Image upload failed");
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['image'];
|
||||
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
$allowed = ['jpg', 'jpeg', 'png'];
|
||||
|
||||
if (!in_array($extension, $allowed)) {
|
||||
error_log("Unsupported file type: $extension");
|
||||
jsonError("Unsupported file type");
|
||||
exit;
|
||||
}
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime_type = finfo_file($finfo, $file['tmp_name']);
|
||||
finfo_close($finfo);
|
||||
|
||||
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/jpg'];
|
||||
if (!in_array($mime_type, $allowed_mime_types)) {
|
||||
error_log("Unsupported MIME type: $mime_type");
|
||||
jsonError("Unsupported file type (MIME mismatch)");
|
||||
exit;
|
||||
}
|
||||
|
||||
$uniqueName = "driver_" . $type . "_" . $driverId . ".$extension";
|
||||
$uploadDir = "../uploads/documents/";
|
||||
$uploadPath = $uploadDir . $uniqueName;
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
if (!move_uploaded_file($file['tmp_name'], $uploadPath)) {
|
||||
error_log("Failed to move uploaded file.");
|
||||
jsonError("Failed to move uploaded image");
|
||||
exit;
|
||||
}
|
||||
|
||||
$host = getenv('APP_DOMAIN') ?: 'api-syria.siromove.com';
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$imageUrl = "$protocol://$host/siro/auth/uploads/documents/" . $uniqueName ;
|
||||
$imageData = file_get_contents($uploadPath);
|
||||
$imageBase64 = base64_encode($imageData);
|
||||
|
||||
$mimeType = match ($extension) {
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
|
||||
$prompts = [
|
||||
"id_front_sy" => <<<EOT
|
||||
You are an OCR expert for Syrian national ID cards (green card).
|
||||
|
||||
### TASK
|
||||
Analyse the **front side** of the ID and return **raw JSON only** with exactly these keys:
|
||||
|
||||
{
|
||||
"full_name": "", // الاسم الثلاثي أو الرباعي
|
||||
"national_number": "", // الرقم الوطني (LATIN digits only)
|
||||
"dob": "YYYY-MM-DD", // تاريخ الميلاد
|
||||
"address": "" // العنوان
|
||||
}
|
||||
|
||||
### RULES
|
||||
* Read the red number on the bottom of the card.
|
||||
* Convert any Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western-Arabic digits (0-9).
|
||||
* `national_number` must contain **Latin digits only, no spaces or other characters**.
|
||||
* If a field is missing, set it to **null**.
|
||||
* Convert the birth date to ISO `YYYY-MM-DD`.
|
||||
* Return valid JSON only — no extra keys, no markdown.
|
||||
EOT,
|
||||
"id_back_sy" => <<<EOT
|
||||
أنت خبير OCR مختص ببطاقات الهوية السورية (الوجه الخلفي).
|
||||
|
||||
### المطلوب
|
||||
حلّل صورة الوجه الخلفي للهوية السورية وأعد **JSON صِرف** يحتوي المفاتيح التالية فقط:
|
||||
|
||||
{
|
||||
"governorate": "", // المحافظة (مثال: دمشق)
|
||||
"address": "", // العنوان التفصيلي (حيّ، بلدة …)
|
||||
"gender": "", //Male or Female
|
||||
"issue_date": "YYYY-MM-DD"// تاريخ الإصدار بصيغة ISO
|
||||
}
|
||||
|
||||
### القواعد
|
||||
1. حوّل أي أرقام عربية شرقية (٠١٢٣٤٥٦٧٨٩) إلى أرقام لاتينية (0-9).
|
||||
2. أعدّ تاريخ الإصدار بالتقويم الميلادي بصيغة `YYYY-MM-DD`.
|
||||
3. استخدم أحرف لاتينية كبيرة لزمرة الدم مع رمز `+` أو `-` فقط.
|
||||
4. إذا كان أحد الحقول غير موجود مطلقًا، أعد قيمته **null**.
|
||||
5. لا تُرجع أي مفاتيح إضافية أو شروح أو Markdown — JSON صالح فقط.
|
||||
EOT,
|
||||
"driving_license_sy_front" => <<<EOT
|
||||
You are an OCR expert for Syrian documents.
|
||||
|
||||
### TASK
|
||||
Analyse the **front side of a Syrian driving licence** and return **clean JSON only** with the following keys (no extra keys, no markdown):
|
||||
|
||||
{
|
||||
"name_arabic": "", // الاسم الثلاثي أو الرباعي بالعربية
|
||||
"birth_place": "", // المحافظة أو المنطقة المكتوبة بعد كلمة الولادة
|
||||
"birth_year": "", // سنة الميلاد فقط (أربعة أرقام)
|
||||
"national_number": ""
|
||||
"civil_registry": "", // سطر "القيد" (مثال: سهوة 3)
|
||||
"blood_type": "" // زمرة الدم بالشكل: A+ , A- , B+ , B- , AB+ , AB- , O+ , O-
|
||||
}
|
||||
|
||||
### RULES
|
||||
* إذا كانت القيمة مفقودة تمامًا اكتب **null**.
|
||||
* لا تُغيّر ترتيب المفاتيح.
|
||||
* لا تُرسل أى شرح أو أسطر إضافية – JSON خالص فقط.
|
||||
EOT,
|
||||
|
||||
"driving_license_sy_back" => <<<EOT
|
||||
You are an OCR expert for Syrian driving licences.
|
||||
|
||||
### TASK
|
||||
Analyse the **back side** of a Syrian driving licence and return **raw JSON only** with exactly these keys:
|
||||
|
||||
{
|
||||
"issue_date": "YYYY-MM-DD", // تاريخ المنح
|
||||
"expiry_date": "YYYY-MM-DD", // صالحة لغاية
|
||||
"license_number": "", // رقم الإجازة
|
||||
"license_category": "" // D1, D2, D3 … (as printed after "UNIVERSAL DRIVING LICENCE")
|
||||
}
|
||||
|
||||
### RULES
|
||||
* If a value is totally absent, set it to **null**.
|
||||
* Convert all dates to ISO `YYYY-MM-DD` (Gregorian).
|
||||
* Do **NOT** add extra keys, comments, or markdown — return valid JSON only.
|
||||
EOT,
|
||||
"vehicle_license_sy_front" => <<<EOT
|
||||
You are an OCR expert specialized in analyzing Syrian vehicle registration cards (الرخصة البرتقالية).
|
||||
|
||||
Your task is to extract structured data from the **front side** of the Syrian orange vehicle card and return **raw JSON only** with the following exact fields:
|
||||
|
||||
{
|
||||
"car_plate": "", // رقم المركبة الكامل مع اسم المحافظة، مأخوذ من الجهة اليسرى في السطر الأول (مثال: "155186 درعا")
|
||||
"owner": "", // اسم المالك الكامل
|
||||
"vin": "", // رقم الهيكل
|
||||
"color": "", // اللون بالعربية أو الإنجليزية (مثال: "أبيض" أو "White")
|
||||
"color_hex": "", // كود اللون بصيغة Hex (مثال: "#FFFFFF") أو #27332F إن تعذّر
|
||||
"issue_date": "YYYY-MM-DD", // تاريخ المنح بصيغة ISO
|
||||
"inspection_date": "YYYY-MM-DD" // تاريخ الفحص القادم بصيغة ISO
|
||||
}
|
||||
|
||||
### Instructions & Rules:
|
||||
|
||||
1. Do **not** extract the "رمز المركبة" (on the right side of the first line) — use only the **left side** of the first line for `car_plate`.
|
||||
2. Convert any Arabic dates (like `2024/05/13`) into ISO format `YYYY-MM-DD`.
|
||||
3. If any value is missing or unreadable, return `null` for it.
|
||||
4. Maintain Arabic encoding (e.g., owner name, city name, color).
|
||||
5. Never guess — extract only what's visually found on the card.
|
||||
6. Never include any explanation or extra output — return the JSON only.
|
||||
|
||||
Example of valid `car_plate`:
|
||||
- "155186 درعا"
|
||||
- "45291 دمشق"
|
||||
- "122334 حمص"
|
||||
EOT,
|
||||
"vehicle_license_sy_back" => <<<EOT
|
||||
You are an OCR expert for Syrian vehicle registration cards (orange card).
|
||||
|
||||
### TASK
|
||||
Analyse the **back side** of the card and return **raw JSON only** with exactly these keys (no more, no less):
|
||||
|
||||
{
|
||||
"make": "", // الصانع (Hyundai …)
|
||||
"model": "", // الطراز (H1 …)
|
||||
"year": "", // سنة الصنع بالأرقام اللاتينية (e.g. "2019")
|
||||
"fuel": "", // نوع الوقود (بنزين، ديزل …) أو بالإنجليزية (Petrol, Diesel,electric)
|
||||
"chassis": "" // رقم الهيكل (VIN)
|
||||
}
|
||||
|
||||
### RULES
|
||||
* Convert any Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western digits (0-9).
|
||||
* Normalise color names to standard English if possible, then map to a common Hex code
|
||||
• "أبيض / White" → **#FFFFFF**
|
||||
• "أسود / Black" → **#000000**
|
||||
• "أحمر / Red" → **#FF0000**
|
||||
• "أزرق / Blue" → **#0000FF**
|
||||
• … (use the closest basic colour); if no match, set **color_hex = null**.
|
||||
* If any field is unreadable or absent, set its value to **null**.
|
||||
* Do **NOT** include extra keys, comments, or markdown — output valid JSON only.
|
||||
EOT
|
||||
];
|
||||
|
||||
$prompt = $prompts[$type] ?? $prompts["id_front_sy"];
|
||||
|
||||
$apiKey = getenv("GEMINI_API_KEY");
|
||||
$apiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=$apiKey";
|
||||
|
||||
$headers = ["Content-Type: application/json"];
|
||||
$payload = [
|
||||
"contents" => [
|
||||
["role" => "user", "parts" => [["text" => $prompt]]],
|
||||
["role" => "user", "parts" => [["inlineData" => ["mimeType" => $mimeType, "data" => $imageBase64]]]]
|
||||
]
|
||||
];
|
||||
|
||||
$ch = curl_init($apiURL);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
$error_msg = curl_error($ch);
|
||||
error_log("CURL error: $error_msg");
|
||||
jsonError("AI Error: $error_msg");
|
||||
curl_close($ch);
|
||||
exit;
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
error_log("AI raw response: $response");
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
error_log("JSON decode error: " . json_last_error_msg());
|
||||
jsonError("Failed to parse AI response");
|
||||
exit;
|
||||
}
|
||||
|
||||
$textRaw = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
$textRaw = trim(preg_replace('/```json|```/', '', $textRaw));
|
||||
$json = json_decode($textRaw, true);
|
||||
|
||||
$requiredKey = match ($type) {
|
||||
'id_front_sy' => 'national_number',
|
||||
'id_back_sy' => 'gender',
|
||||
'driving_license_sy' => 'license_type',
|
||||
'vehicle_license_sy' => 'chassis',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (!$json || ($requiredKey && !isset($json[$requiredKey]))) {
|
||||
error_log("AI response missing required key '$requiredKey': $textRaw");
|
||||
jsonError("AI failed to extract required information");
|
||||
exit;
|
||||
}
|
||||
|
||||
printSuccess([
|
||||
"image_url" => $imageUrl,
|
||||
"data" => $json
|
||||
]);
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// Sanitize and validate input
|
||||
$driverId = filterRequest("driverId");
|
||||
$issueDate = filterRequest("IssueDate");
|
||||
$inspectionResult = filterRequest("InspectionResult");
|
||||
|
||||
// Prepare SQL statement
|
||||
$sql = "INSERT INTO criminalDocuments (driverId, IssueDate, InspectionResult)
|
||||
VALUES (:driverId, :issueDate, :inspectionResult)";
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare($sql);
|
||||
|
||||
// Bind parameters
|
||||
$stmt->bindParam(':driverId', $driverId, PDO::PARAM_INT);
|
||||
$stmt->bindParam(':issueDate', $issueDate, PDO::PARAM_STR);
|
||||
$stmt->bindParam(':inspectionResult', $inspectionResult, PDO::PARAM_STR);
|
||||
|
||||
// Execute the statement
|
||||
$stmt->execute();
|
||||
|
||||
// Check if the insertion was successful
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess(null, "Criminal document data saved successfully");
|
||||
} else {
|
||||
jsonError("Failed to save criminal document data");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// Log the error and print a generic failure message
|
||||
error_log("Database Error: " . $e->getMessage());
|
||||
jsonError("An error occurred while saving the data");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// استقبال وتشفير رقم الهاتف
|
||||
$phoneNumber = filterRequest("phone_number");
|
||||
$phoneNumber = otpPhoneKey($phoneNumber);
|
||||
|
||||
// تجهيز الاستعلام باستخدام bindParam للحماية
|
||||
$sql = "SELECT * FROM `phone_verification` WHERE `phone_number` = :phone_number";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(":phone_number", $phoneNumber);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
jsonSuccess($rows);
|
||||
} else {
|
||||
jsonError("No phone verified yet found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$id = filterRequest("id");
|
||||
// يمكن استقبال سبب الحظر من التطبيق أو وضعه كقيمة افتراضية
|
||||
$reason = "Driver requested deletion (deleteFromHimself)";
|
||||
|
||||
// تأكد أن المعرف رقم صحيح
|
||||
if (!is_numeric($id)) {
|
||||
jsonError("Invalid ID");
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. جلب رقم الهاتف الخاص بالسائق قبل التحديث
|
||||
// نحتاج الهاتف لإضافته في القائمة السوداء
|
||||
$stmtPhone = $con->prepare("SELECT phone FROM `driver` WHERE `id` = :id");
|
||||
$stmtPhone->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stmtPhone->execute();
|
||||
$driverData = $stmtPhone->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// التحقق من وجود السائق
|
||||
if (!$driverData) {
|
||||
jsonError("Driver not found");
|
||||
exit();
|
||||
}
|
||||
|
||||
$phone = $driverData['phone'];
|
||||
|
||||
// 2. تحديث حالة السائق
|
||||
$sql = "UPDATE `driver` SET `status` = 'deleteFromHimself' WHERE `id` = :id";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// 3. الإضافة إلى القائمة السوداء (blacklist_driver)
|
||||
// نستخدم NOW() لتسجيل الوقت الحالي تلقائياً
|
||||
// لا نمرر id العمود الأول لأنه غالباً Auto Increment في قاعدة البيانات
|
||||
$insertSql = "INSERT INTO `blacklist_driver` (`driver_id`, `phone`, `reason`, `created_at`)
|
||||
VALUES (:driver_id, :phone, :reason, NOW())";
|
||||
|
||||
$insertStmt = $con->prepare($insertSql);
|
||||
$insertStmt->execute([
|
||||
':driver_id' => $id,
|
||||
':phone' => $phone,
|
||||
':reason' => $reason
|
||||
]);
|
||||
|
||||
jsonSuccess(null, "Record marked as deleted and added to blacklist successfully");
|
||||
} else {
|
||||
jsonError("Failed to update record or no change made");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// في حال حدوث خطأ في قاعدة البيانات (مثلاً تكرار الإضافة)
|
||||
error_log("[deletecaptainAccounr] " . $e->getMessage());
|
||||
jsonError("Database Error");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
// 🔥 [Fix Broken Access Control] كان يتحقق من صلاحية التوكن فقط — أي مستخدم
|
||||
// مسجّل دخول (راكب/سائق آخر) كان يقدر يجلب بيانات أي سائق مفكوكة التشفير
|
||||
// (هوية وطنية، هاتف، عنوان...) بالإضافة لروابط وثائقه الشخصية.
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$driverId = filterRequest("id");
|
||||
|
||||
if (empty($driverId)) {
|
||||
jsonError("driver_id is required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// تفاصيل السائق
|
||||
$sql = "SELECT * FROM driver WHERE id = :id LIMIT 1";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([':id' => $driverId]);
|
||||
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$driver) {
|
||||
jsonError("Driver not found.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// فك التشفير للحقول الحساسة
|
||||
foreach ($driver as $k => $v) {
|
||||
if (in_array($k, ['phone',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'national_number',
|
||||
'address','gender','site',
|
||||
'birthdate',
|
||||
'name_arabic'])) {
|
||||
$driver[$k] = $encryptionHelper->decryptData($v);
|
||||
}
|
||||
}
|
||||
|
||||
// الوثائق
|
||||
$sql2 = "SELECT doc_type, image_name, link FROM driver_documents WHERE driverID = :id";
|
||||
$stmt2 = $con->prepare($sql2);
|
||||
$stmt2->execute([':id' => $driverId]);
|
||||
$docs = $stmt2->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
printSuccess([
|
||||
"driver" => $driver,
|
||||
"documents" => $docs
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log("[driver_details] " . $e->getMessage());
|
||||
jsonError("Error fetching details");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
// 🔥 [Fix Broken Access Control] كان يتحقق من صلاحية التوكن فقط بدون التحقق
|
||||
// من الدور — أي توكن صالح (حتى راكب) كان يقدر يسحب قائمة السائقين المعلّقين
|
||||
// وبياناتهم الشخصية المفكوكة التشفير.
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$limit = isset($_POST['limit']) ? (int)$_POST['limit'] : (isset($_GET['limit']) ? (int)$_GET['limit'] : 10);
|
||||
$offset = isset($_POST['offset']) ? (int)$_POST['offset'] : (isset($_GET['offset']) ? (int)$_GET['offset'] : 0);
|
||||
|
||||
try {
|
||||
$sql = "SELECT id, first_name, last_name, phone FROM driver WHERE status <> 'active' ORDER BY id DESC LIMIT :limit OFFSET :offset";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// فك التشفير
|
||||
foreach ($rows as &$r) {
|
||||
$r['phone'] = $encryptionHelper->decryptData($r['phone']);
|
||||
$r['first_name'] = $encryptionHelper->decryptData($r['first_name']);
|
||||
$r['last_name'] = $encryptionHelper->decryptData($r['last_name']);
|
||||
}
|
||||
|
||||
jsonSuccess($rows); // يرجع كـ message: [...]
|
||||
} catch (PDOException $e) {
|
||||
error_log("[drivers_pending_list] " . $e->getMessage());
|
||||
jsonError("Error fetching data");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$driverID = filterRequest("id");
|
||||
|
||||
// تحقق أن المعرف رقم صحيح
|
||||
if (!is_numeric($driverID)) {
|
||||
jsonError("Invalid driver ID");
|
||||
exit();
|
||||
}
|
||||
|
||||
// استخدم bindParam لتفادي حقن SQL
|
||||
$sql = "SELECT `accountBank` FROM `driver` WHERE `id` = :id";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':id', $driverID, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
jsonSuccess($row);
|
||||
} else {
|
||||
jsonError("No account bank record found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// $driverID = filterRequest("id");
|
||||
|
||||
$sql = "
|
||||
SELECT * FROM `promptDriverIDEgypt`";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Fetch the record
|
||||
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
jsonSuccess($row);
|
||||
|
||||
}
|
||||
else{
|
||||
// Print a failure message
|
||||
jsonError($message = "No wallet record found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
$phoneNumber = filterRequest("phone_number");
|
||||
|
||||
// تشفير الرقم قبل البحث
|
||||
$phoneNumber_encrypted = otpPhoneKey($phoneNumber);
|
||||
|
||||
try {
|
||||
// الاستعلام عن السائق حسب رقم الهاتف وحالة التحقق
|
||||
$stmt = $con->prepare("
|
||||
SELECT * FROM phone_verification
|
||||
WHERE phone_number = ? AND is_verified = 1
|
||||
");
|
||||
$stmt->execute([$phoneNumber_encrypted]);
|
||||
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($driver) {
|
||||
jsonSuccess(null, "Phone number is verified.");
|
||||
} else {
|
||||
jsonError("Phone number is not verified or does not exist.");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[isPhoneVerified] " . $e->getMessage());
|
||||
jsonError("Database error");
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
global $blindIndex;
|
||||
|
||||
$email = filterRequest('email');
|
||||
$phone = filterRequest('phone');
|
||||
$password = filterRequest('password');
|
||||
|
||||
if (empty($phone) && empty($email)) {
|
||||
jsonError("Phone or email is required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if (!empty($phone)) {
|
||||
$phoneEnc = $encryptionHelper->encryptData($phone);
|
||||
$conditions[] = "driver.phone = :phone";
|
||||
$params[':phone'] = $phoneEnc;
|
||||
|
||||
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $phone) : null;
|
||||
if ($phoneBidx) {
|
||||
$conditions[] = "driver.phone_bidx = :phone_bidx";
|
||||
$params[':phone_bidx'] = $phoneBidx;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($email)) {
|
||||
$emailEnc = $encryptionHelper->encryptData($email);
|
||||
$conditions[] = "driver.email = :email";
|
||||
$params[':email'] = $emailEnc;
|
||||
|
||||
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
|
||||
if ($emailBidx) {
|
||||
$conditions[] = "driver.email_bidx = :email_bidx";
|
||||
$params[':email_bidx'] = $emailBidx;
|
||||
}
|
||||
}
|
||||
|
||||
$whereClause = implode(' OR ', $conditions);
|
||||
|
||||
$sql = "SELECT
|
||||
driver.id,
|
||||
driver.phone,
|
||||
driver.email,
|
||||
driver.password,
|
||||
driver.gender,
|
||||
driver.birthdate,
|
||||
driver.site,
|
||||
driver.first_name,
|
||||
driver.last_name,
|
||||
driver.education,
|
||||
driver.employmentType,
|
||||
driver.maritalStatus,
|
||||
driver.created_at,
|
||||
driver.updated_at,
|
||||
driver.email AS _email_enc
|
||||
FROM
|
||||
driver
|
||||
WHERE
|
||||
$whereClause";
|
||||
|
||||
|
||||
/**
|
||||
* حالة توثيق البريد.
|
||||
*
|
||||
* كان الاستعلام يربط email_verifications.email بعمود البريد في الحساب، لكن
|
||||
* الأول يُخزَّن نصاً صريحاً والثاني مشفّراً — فالربط لم يكن يطابق شيئاً أصلاً
|
||||
* وكانت verified تعود NULL دائماً. نجلبها هنا بالبريد الأصلي.
|
||||
*/
|
||||
function fetchEmailVerified(PDO $con, ?string $plainEmail): ?int
|
||||
{
|
||||
if (!$plainEmail) return null;
|
||||
try {
|
||||
$st = $con->prepare("SELECT verified FROM email_verifications WHERE email = ? LIMIT 1");
|
||||
$st->execute([$plainEmail]);
|
||||
$v = $st->fetchColumn();
|
||||
return $v === false ? null : (int) $v;
|
||||
} catch (PDOException $e) {
|
||||
error_log('[email_verifications] ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$count = count($data);
|
||||
|
||||
if ($count > 0) {
|
||||
$plainEmail = $encryptionHelper->decryptData($data[0]['_email_enc'] ?? null) ?: null;
|
||||
$data[0]['verified'] = fetchEmailVerified($con, $plainEmail);
|
||||
unset($data[0]['_email_enc']);
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
$stored_password = $data[0]['password'];
|
||||
if (password_verify($password, $stored_password)) {
|
||||
|
||||
// فك التشفير للحقول الحساسة
|
||||
$data[0]['phone'] = $encryptionHelper->decryptData($data[0]['phone']);
|
||||
$data[0]['email'] = $encryptionHelper->decryptData($data[0]['email']);
|
||||
$data[0]['gender'] = $encryptionHelper->decryptData($data[0]['gender']);
|
||||
$data[0]['birthdate'] = $encryptionHelper->decryptData($data[0]['birthdate']);
|
||||
$data[0]['site'] = $encryptionHelper->decryptData($data[0]['site']);
|
||||
$data[0]['first_name'] = $encryptionHelper->decryptData($data[0]['first_name']);
|
||||
$data[0]['last_name'] = $encryptionHelper->decryptData($data[0]['last_name']);
|
||||
$data[0]['education'] = $encryptionHelper->decryptData($data[0]['education']);
|
||||
$data[0]['employmentType'] = $encryptionHelper->decryptData($data[0]['employmentType']);
|
||||
$data[0]['maritalStatus'] = $encryptionHelper->decryptData($data[0]['maritalStatus']);
|
||||
|
||||
unset($data[0]['password']); // لا نرجّع الباسورد
|
||||
jsonSuccess($data);
|
||||
} else {
|
||||
jsonError("Incorrect password.");
|
||||
}
|
||||
} else {
|
||||
jsonError("User does not exist.");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
// loginFromGoogle.php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
try {
|
||||
/* ────────────────────────────────
|
||||
1) استخدام ID: من الطلب أولاً، ثم من JWT
|
||||
───────────────────────────────── */
|
||||
$driverID = filterRequest('driver_id') ?: $user_id;
|
||||
|
||||
error_log("[Debug] DriverID from JWT: $driverID");
|
||||
|
||||
/* ────────────────────────────────
|
||||
3) إعداد الاستعلام الموحَّد
|
||||
───────────────────────────────── */
|
||||
$sql = "
|
||||
SELECT
|
||||
driver.id, driver.phone, driver.email, driver.gender, driver.birthdate,
|
||||
driver.site, driver.first_name, driver.last_name, driver.bankCode,
|
||||
driver.accountBank, driver.employmentType,driver.status, driver.maritalStatus,
|
||||
driver.created_at, driver.updated_at,
|
||||
phone_verification.is_verified,
|
||||
CarRegistration.make, CarRegistration.model, CarRegistration.year,
|
||||
df.is_claimed, inv.isInstall, inv.isGiftToken
|
||||
FROM driver
|
||||
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone_key
|
||||
LEFT JOIN driver_gifts df ON df.driver_id = driver.id
|
||||
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
|
||||
LEFT JOIN invites inv ON inv.driverId = driver.id
|
||||
WHERE
|
||||
driver.id = :id
|
||||
-- AND phone_verification.is_verified = '1'
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
// error_log("[Debug] queryString:\n$sql");
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
|
||||
// باراميترات الربط
|
||||
$params = [
|
||||
':id' => $driverID,
|
||||
];
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v);
|
||||
}
|
||||
|
||||
/* ───────── dumpParams (اختياري) ───────── */
|
||||
ob_start();
|
||||
$stmt->debugDumpParams();
|
||||
error_log("[Debug] dumpParams:\n" . ob_get_clean());
|
||||
|
||||
/* ────────────────────────────────
|
||||
4) تنفيذ الاستعلام
|
||||
───────────────────────────────── */
|
||||
$stmt->execute();
|
||||
error_log("[Debug] stmt->rowCount(): " . $stmt->rowCount());
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// error_log("[Debug] Raw fetched JSON: " . json_encode($rows, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (!$rows) {
|
||||
jsonError("User does not exist or phone not verified.");
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────
|
||||
5) فك التشفير للحقول الحسّاسة
|
||||
───────────────────────────────── */
|
||||
$data = &$rows[0]; // مرجع لتوفير الذاكرة
|
||||
|
||||
$decryptIfNotNull = function($field) use (&$data, $encryptionHelper) {
|
||||
if (isset($data[$field]) && $data[$field] !== null) {
|
||||
$data[$field] = $encryptionHelper->decryptData($data[$field]);
|
||||
}
|
||||
};
|
||||
|
||||
foreach ([
|
||||
'phone', 'email', 'gender', 'birthdate', 'site',
|
||||
'first_name', 'last_name'
|
||||
] as $field) {
|
||||
$decryptIfNotNull($field);
|
||||
}
|
||||
error_log("[Debug] Raw fetched JSON: " . json_encode($rows, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
"count" => 1,
|
||||
"data" => $rows // نتيجة واحدة فقط
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
} catch (PDOException $e) {
|
||||
error_log("[PDO ERROR] " . $e->getMessage());
|
||||
jsonError("Database error: ".$e->getCode());
|
||||
} catch (Exception $e) {
|
||||
error_log("[GENERAL ERROR] " . $e->getMessage());
|
||||
jsonError("Error occurred.");
|
||||
} finally {
|
||||
$stmt = null;
|
||||
$con = null;
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// loginUsingCredentialsWithoutGoogle.php
|
||||
// مخصص لدخول الفاحصين (Testers) بالإيميل والباسورد
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
$email = filterRequest('email');
|
||||
$password = filterRequest('password');
|
||||
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
|
||||
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
||||
|
||||
// 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');
|
||||
|
||||
// تشفير الإيميل لاستخدامه في الاستعلام
|
||||
$encryptedEmail = $encryptionHelper->encryptData($email);
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
// Auto-seed/create tester driver logic removed for security
|
||||
|
||||
global $blindIndex;
|
||||
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $email) : null;
|
||||
|
||||
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر أو الفهرس الأعمى
|
||||
$sql = "SELECT
|
||||
driver.*,
|
||||
phone_verification.is_verified,
|
||||
CarRegistration.make,
|
||||
CarRegistration.model,
|
||||
CarRegistration.year
|
||||
FROM driver
|
||||
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone_key
|
||||
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
|
||||
WHERE
|
||||
driver.email = :email OR (:email_bidx IS NOT NULL AND driver.email_bidx = :email_bidx)
|
||||
LIMIT 1";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([':email' => $encryptedEmail, ':email_bidx' => $emailBidx]);
|
||||
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($data) {
|
||||
// التحقق من أن الحساب معلم كحساب فحص في قاعدة البيانات أو البيئة
|
||||
$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();
|
||||
}
|
||||
// فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين)
|
||||
// لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt
|
||||
if (password_verify($password, $data['password'])) {
|
||||
unset($data['password']);
|
||||
|
||||
// فك تشفير الحقول الحساسة
|
||||
$data['phone'] = $encryptionHelper->decryptData($data['phone']);
|
||||
$data['email'] = $encryptionHelper->decryptData($data['email']);
|
||||
$data['gender'] = $encryptionHelper->decryptData($data['gender']);
|
||||
$data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
|
||||
$data['site'] = $encryptionHelper->decryptData($data['site']);
|
||||
$data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
|
||||
$data['last_name'] = $encryptionHelper->decryptData($data['last_name']);
|
||||
if(isset($data['employmentType'])) $data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
|
||||
if(isset($data['maritalStatus'])) $data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
|
||||
|
||||
// توليد الـ 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 {
|
||||
jsonError("Incorrect password.");
|
||||
}
|
||||
} else {
|
||||
jsonError("User does not exist.");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("[Tester Login Error] " . $e->getMessage());
|
||||
jsonError("Server error occurred.");
|
||||
} finally {
|
||||
$stmt = null;
|
||||
$con = null;
|
||||
}
|
||||
exit();
|
||||
?>
|
||||
@@ -0,0 +1,635 @@
|
||||
<?php
|
||||
/**
|
||||
* Endpoint: register_driver_and_car.php
|
||||
* [MODIFIED] Added vehicle_category_id and fuel_type_id support.
|
||||
* [MODIFIED] Fixed birthdate logic: Append -01-01 BEFORE encryption.
|
||||
* [MODIFIED] Added Syrian phone number formatting logic.
|
||||
*/
|
||||
//register_driver_and_car.php
|
||||
$allowRegistration = true;
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Rate Limiting: الحماية من التسجيل العشوائي وهجمات الـ Bots
|
||||
$rateLimiter = new RateLimiter($redis);
|
||||
$rateLimiter->enforce(RateLimiter::identifier(), 'register_driver');
|
||||
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonError("Invalid method.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$host = getenv('APP_DOMAIN') ?: 'api-syria.siromove.com';
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$PUBLIC_BASE = "$protocol://$host/siro/auth/uploads/documents";
|
||||
|
||||
/* ================== 1) Input Fields ================== */
|
||||
$raw_first_name = null;
|
||||
$raw_last_name = null;
|
||||
|
||||
$required = ["phone", "password", "first_name", "last_name"];
|
||||
$optional = [
|
||||
"id","email","gender","license_type","national_number",
|
||||
"name_arabic","issue_date","expiry_date","license_categories",
|
||||
"address","licenseIssueDate","status","birthdate","site",
|
||||
"employmentType","maritalStatus","fullNameMaritial","expirationDate"
|
||||
];
|
||||
$carRequired = [
|
||||
"vin","car_plate","make","model","year","expiration_date",
|
||||
"color","owner","color_hex","fuel"
|
||||
];
|
||||
// حقول اختيارية للسيارة (التصنيف والوقود الرقمي)
|
||||
// vehicle_category_id, fuel_type_id
|
||||
|
||||
$docKeys = [
|
||||
'id_front',
|
||||
'id_back',
|
||||
'driver_license',
|
||||
'driver_license_back',
|
||||
'profile_picture',
|
||||
'criminal_record',
|
||||
'car_license_front',
|
||||
'car_license_back'
|
||||
];
|
||||
|
||||
// Read driver fields
|
||||
$data = [];
|
||||
foreach ($required as $f) {
|
||||
$v = filterRequest($f);
|
||||
if ($v === null || $v === '') {
|
||||
jsonError("Missing required field: $f");
|
||||
exit;
|
||||
}
|
||||
$data[$f] = $v;
|
||||
|
||||
if ($f === 'first_name') $raw_first_name = $v;
|
||||
if ($f === 'last_name') $raw_last_name = $v;
|
||||
}
|
||||
foreach ($optional as $f) {
|
||||
$v = filterRequest($f);
|
||||
$data[$f] = ($v === null || $v === '' || $v === 'Not specified') ? null : $v;
|
||||
}
|
||||
|
||||
/* ================== 🟢 START PHONE FORMATTING LOGIC 🟢 ================== */
|
||||
$country = 'Syria'; // Default
|
||||
if (!empty($data['phone'])) {
|
||||
$phone = $data['phone'];
|
||||
|
||||
// 1. إزالة المسافات والرموز
|
||||
$phone = preg_replace('/[ \-\(\)\+]/', '', $phone);
|
||||
$phone = trim($phone);
|
||||
|
||||
if (strpos($phone, '962') === 0 || strpos($phone, '00962') === 0) {
|
||||
if (strpos($phone, '00962') === 0) $phone = substr($phone, 2);
|
||||
$country = 'Jordan';
|
||||
} elseif (strpos($phone, '20') === 0 || strpos($phone, '0020') === 0) {
|
||||
if (strpos($phone, '0020') === 0) $phone = substr($phone, 2);
|
||||
$country = 'Egypt';
|
||||
} else {
|
||||
// 2. توحيد البادئات الدولية (سوريا)
|
||||
if (strpos($phone, '00963') === 0) {
|
||||
$phone = substr($phone, 2);
|
||||
} elseif (strpos($phone, '0963') === 0) {
|
||||
$phone = substr($phone, 1);
|
||||
}
|
||||
|
||||
// 3. معالجة الحالات الخاصة بالصفر الزائد بعد الرمز الدولي
|
||||
if (strpos($phone, '96309') === 0) {
|
||||
$phone = '9639' . substr($phone, 5);
|
||||
}
|
||||
elseif (strpos($phone, '9630') === 0) {
|
||||
$phone = '9639' . substr($phone, 4);
|
||||
}
|
||||
|
||||
// 4. معالجة الأرقام المحلية
|
||||
elseif (strpos($phone, '09') === 0) {
|
||||
$phone = '963' . substr($phone, 1);
|
||||
}
|
||||
elseif (strpos($phone, '9') === 0 && strlen($phone) == 9) {
|
||||
$phone = '963' . $phone;
|
||||
}
|
||||
elseif (strpos($phone, '0') === 0 && strlen($phone) == 10) {
|
||||
$phone = '963' . substr($phone, 1);
|
||||
}
|
||||
|
||||
// 5. التأكد من وجود 9 بعد الرمز الدولي
|
||||
if (strpos($phone, '963') === 0 && strlen($phone) > 3) {
|
||||
if (strpos($phone, '9639') !== 0) {
|
||||
$phone = '9639' . substr($phone, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
$data['phone'] = $phone;
|
||||
}
|
||||
/* ================== 🔴 END PHONE FORMATTING LOGIC 🔴 ================== */
|
||||
|
||||
// ======================================================
|
||||
// Step 1.5: التحقق الفعلي من ملكية رقم الهاتف قبل إكمال المعالجة (سد الثغرة)
|
||||
// ======================================================
|
||||
require_once __DIR__ . '/../../../core/Auth/EncryptionHelper.php';
|
||||
$tempEncryptionHelper = new EncryptionHelper($redis);
|
||||
$phoneNumber_encrypted_check = $tempEncryptionHelper->encryptData($data['phone']);
|
||||
|
||||
$verifyCheckStmt = $con->prepare(
|
||||
"SELECT id FROM phone_verification_driver
|
||||
WHERE phone_number = ? AND verified = 1 AND created_at > DATE_SUB(NOW(), INTERVAL 30 MINUTE)
|
||||
LIMIT 1"
|
||||
);
|
||||
$verifyCheckStmt->execute([$phoneNumber_encrypted_check]);
|
||||
if ($verifyCheckStmt->rowCount() === 0) {
|
||||
error_log("[Register_Debug_driver] Error: Phone number not verified via OTP.");
|
||||
jsonError("Phone number must be verified before registration.");
|
||||
exit();
|
||||
}
|
||||
// ======================================================
|
||||
|
||||
// تجهيز تاريخ الميلاد قبل التشفير
|
||||
if (!empty($data['birthdate'])) {
|
||||
$data['birthdate'] = trim($data['birthdate']);
|
||||
$data['birthdate'] = $data['birthdate'] . '-01-01';
|
||||
} else {
|
||||
$data['birthdate'] = '1970-01-01';
|
||||
}
|
||||
|
||||
// Read car fields
|
||||
$car = [];
|
||||
foreach ($carRequired as $f) {
|
||||
$v = filterRequest($f);
|
||||
if ($v === null || $v === '') {
|
||||
jsonError("Missing required field: $f");
|
||||
exit;
|
||||
}
|
||||
$car[$f] = $v;
|
||||
}
|
||||
|
||||
// Read document links
|
||||
$docUrls = [];
|
||||
foreach ($docKeys as $k) {
|
||||
$u = filterRequest($k);
|
||||
if (($k === 'driver_license_back' || $k === 'criminal_record') && ($u === null || $u === '')) continue;
|
||||
if ($u === null || $u === '') {
|
||||
jsonError("Missing document URL: $k");
|
||||
exit;
|
||||
}
|
||||
if (!filter_var($u, FILTER_VALIDATE_URL)) {
|
||||
jsonError("Invalid document URL: $k");
|
||||
exit;
|
||||
}
|
||||
$docUrls[$k] = $u;
|
||||
}
|
||||
|
||||
/* ================== حفظ المدخلات الخام قبل AI ================== */
|
||||
$userInputJson = json_encode([
|
||||
'driver' => $data,
|
||||
'car' => $car,
|
||||
]);
|
||||
|
||||
/* ================== AI PROCESSING START ================== */
|
||||
$apiKey = getenv("GEMINI_API_KEY");
|
||||
$aiRawText = null;
|
||||
if ($apiKey) {
|
||||
$promptBase = '
|
||||
You are a highly secure AI Assistant specialized in analyzing identification and driver documents.
|
||||
Country Context: ' . ($country ?? 'Syria') . '
|
||||
|
||||
### TASK
|
||||
We are providing you with multiple images representing a driver\'s documents (National ID, Driver License, Profile Picture, Criminal Record, and Car Registration).
|
||||
Extract all the required data accurately according to the exact schema provided, and perform a FACE MATCHING analysis.
|
||||
Since the driver may be from Jordan, Egypt, or Syria, the layout, fields, and distribution of information between the front and back of each card varies widely by country.
|
||||
Therefore, do NOT assume a specific field is on the front or the back of a card. You must scan all provided document images (e.g., both ID images, both license images) and intelligently locate the requested fields wherever they appear on the cards.
|
||||
|
||||
### RULES
|
||||
1. Convert any Eastern-Arabic digits (٠١٢٣٤٥٦٧٨٩) to Western digits (0-9).
|
||||
2. Dates must be formatted as ISO `YYYY-MM-DD`.
|
||||
3. Smart Extraction: Scan all provided document images without restriction. Do not fail the overall request if some optional fields (like governorate or address or issue dates) are missing or unreadable on the card. Simply set those specific fields to `null` in the JSON, but do NOT set the overall status to failure. Overall status should only be \'failure\' if there is a critical security/authenticity issue (e.g., face mismatch, fake/forged documents, or missing primary driver identity).
|
||||
4. FACE MATCHING (CRITICAL): Compare the face in the "Profile Picture" with the photos on the "National ID" and "Driver License".
|
||||
5. Ensure the Criminal/Non-Conviction record is valid and matches the driver\'s name.
|
||||
6. The `national_number` and `vin` (chassis) must contain Latin digits/characters only.
|
||||
7. Normalize color names (e.g. "أبيض" -> "White") and provide a matching Hex code (e.g. "#FFFFFF").
|
||||
8. Do NOT add markdown formatting around the output. Return ONLY raw JSON.
|
||||
|
||||
### REQUIRED JSON OUTPUT FORMAT
|
||||
{
|
||||
"status": "success|failure",
|
||||
"reason": "If failure, state the reason (e.g., Face mismatch, blurry, invalid record)",
|
||||
"face_match_confidence": "high|low",
|
||||
"driver": {
|
||||
"full_name": "", // Full name in Arabic
|
||||
"national_number": "", // National ID/National number (Latin digits)
|
||||
"dob": "YYYY-MM-DD", // Date of birth
|
||||
"address": "", // Full address
|
||||
"governorate": "", // Governorate/Site/City
|
||||
"gender": "Male|Female", // Gender
|
||||
"id_issue_date": "YYYY-MM-DD", // National ID issue date
|
||||
"license_issue_date": "YYYY-MM-DD", // Driver license issue date
|
||||
"license_expiry_date": "YYYY-MM-DD", // Driver license expiry date
|
||||
"license_number": "", // Driver license number
|
||||
"license_category": "", // License category (e.g., D1, B, Private, Public)
|
||||
"blood_type": "", // Blood type (e.g., A+, O-)
|
||||
"civil_registry": "", // Civil registry/Place of registration
|
||||
"birth_place": "" // Place of birth
|
||||
},
|
||||
"car": {
|
||||
"car_plate": "", // Full car plate (e.g., 155186 درعا)
|
||||
"owner": "", // Owner full name
|
||||
"vin": "", // Chassis/VIN number
|
||||
"color": "", // Color name
|
||||
"color_hex": "", // Color hex code (e.g., #FFFFFF)
|
||||
"car_issue_date": "YYYY-MM-DD", // Car registration issue date
|
||||
"inspection_date": "YYYY-MM-DD", // Car next inspection date
|
||||
"make": "", // Car Make (e.g., Hyundai)
|
||||
"model": "", // Car Model (e.g., H1)
|
||||
"year": "", // Manufacturing year (e.g., 2019)
|
||||
"fuel": "" // Fuel type (e.g., Petrol, Diesel, Electric)
|
||||
}
|
||||
}';
|
||||
|
||||
$contents = [
|
||||
["role" => "user", "parts" => [["text" => $promptBase]]]
|
||||
];
|
||||
|
||||
// ✅ SSRF Protection: Allowlist for document URLs
|
||||
$allowedHosts = array_filter([
|
||||
parse_url($PUBLIC_BASE, PHP_URL_HOST),
|
||||
getenv('ALLOWED_UPLOAD_HOST'),
|
||||
]);
|
||||
$maxFileSize = 10 * 1024 * 1024; // 10MB max per image
|
||||
|
||||
foreach ($docUrls as $key => $url) {
|
||||
$urlHost = parse_url($url, PHP_URL_HOST);
|
||||
$allowed = false;
|
||||
foreach ($allowedHosts as $host) {
|
||||
if ($host && $urlHost === $host) {
|
||||
$allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$allowed) {
|
||||
error_log("[SSRF_BLOCKED] Doc URL not in allowlist: $urlHost ($key)");
|
||||
continue;
|
||||
}
|
||||
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'timeout' => 10,
|
||||
'ignore_errors' => true,
|
||||
]]);
|
||||
$imgData = @file_get_contents($url, false, $ctx, 0, $maxFileSize);
|
||||
if ($imgData !== false) {
|
||||
$base64 = base64_encode($imgData);
|
||||
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
$mime = ($ext === 'png') ? 'image/png' : 'image/jpeg';
|
||||
$contents[0]["parts"][] = ["text" => "Image type: " . $key];
|
||||
$contents[0]["parts"][] = ["inlineData" => ["mimeType" => $mime, "data" => $base64]];
|
||||
}
|
||||
}
|
||||
|
||||
$apiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=$apiKey";
|
||||
$payload = ["contents" => $contents];
|
||||
|
||||
$ch = curl_init($apiURL);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response) {
|
||||
$aiData = json_decode($response, true);
|
||||
$textRaw = $aiData['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
$textRaw = trim(preg_replace('/```json|```/', '', $textRaw));
|
||||
$json = json_decode($textRaw, true);
|
||||
$aiRawText = ($json !== null && $json !== false) ? $textRaw : null; // فقط إذا كان JSON صحيح
|
||||
|
||||
if ($json && isset($json['status']) && strtolower($json['status']) === 'failure') {
|
||||
jsonError("AI Verification Failed: " . ($json['reason'] ?? 'Unknown reason'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($json && isset($json['driver'])) {
|
||||
$ex = $json['driver'];
|
||||
if (!empty($ex['full_name'])) {
|
||||
$data['name_arabic'] = $ex['full_name'];
|
||||
$parts = explode(' ', trim($ex['full_name']));
|
||||
if (count($parts) >= 2) {
|
||||
$data['first_name'] = $parts[0];
|
||||
$data['last_name'] = implode(' ', array_slice($parts, 1));
|
||||
} else {
|
||||
$data['first_name'] = $ex['full_name'];
|
||||
$data['last_name'] = '';
|
||||
}
|
||||
}
|
||||
if (!empty($ex['national_number'])) $data['national_number'] = $ex['national_number'];
|
||||
if (!empty($ex['dob'])) {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $ex['dob'])) {
|
||||
$data['birthdate'] = $ex['dob'];
|
||||
} elseif (preg_match('/^\d{4}$/', $ex['dob'])) {
|
||||
$data['birthdate'] = $ex['dob'] . '-01-01';
|
||||
}
|
||||
}
|
||||
if (!empty($ex['address'])) $data['address'] = $ex['address'];
|
||||
if (!empty($ex['governorate'])) $data['site'] = $ex['governorate'];
|
||||
if (!empty($ex['gender'])) $data['gender'] = $ex['gender'];
|
||||
if (!empty($ex['id_issue_date'])) $data['issue_date'] = $ex['id_issue_date'];
|
||||
if (!empty($ex['license_issue_date'])) $data['licenseIssueDate'] = $ex['license_issue_date'];
|
||||
if (!empty($ex['license_expiry_date'])) $data['expiry_date'] = $ex['license_expiry_date'];
|
||||
if (!empty($ex['license_category'])) $data['license_categories'] = $ex['license_category'];
|
||||
// Not mapped directly in basic DB schema but extracted: blood_type, civil_registry, birth_place
|
||||
}
|
||||
|
||||
if ($json && isset($json['car'])) {
|
||||
$ex = $json['car'];
|
||||
if (!empty($ex['car_plate'])) $car['car_plate'] = $ex['car_plate'];
|
||||
if (!empty($ex['make'])) $car['make'] = $ex['make'];
|
||||
if (!empty($ex['model'])) $car['model'] = $ex['model'];
|
||||
if (!empty($ex['year'])) $car['year'] = $ex['year'];
|
||||
if (!empty($ex['color'])) $car['color'] = $ex['color'];
|
||||
if (!empty($ex['color_hex'])) $car['color_hex'] = $ex['color_hex'];
|
||||
if (!empty($ex['vin'])) $car['vin'] = $ex['vin'];
|
||||
if (!empty($ex['owner'])) $car['owner'] = $ex['owner'];
|
||||
if (!empty($ex['fuel'])) $car['fuel'] = $ex['fuel'];
|
||||
}
|
||||
}
|
||||
}
|
||||
/* ================== AI PROCESSING END ================== */
|
||||
|
||||
/* ================== 2) Generate default id/email ================== */
|
||||
if (empty($data['id'])) {
|
||||
$data['id'] = 'DRV' . date('YmdHis') . random_int(1000, 9999);
|
||||
}
|
||||
if ($data['email'] === null) {
|
||||
$data['email'] = $data['phone'] . '@intaleqapp.com';
|
||||
}
|
||||
|
||||
/* ================== 3) Hash password (HMAC + password_hash) ================== */
|
||||
// 🔴 مهم: يجب أن يكون قبل التشفير - يستخدم القيم الخام
|
||||
$pepper = getenv('SECRET_KEY_HMAC');
|
||||
$baseParts = [
|
||||
$data['id'],
|
||||
$data['phone'],
|
||||
];
|
||||
if (!empty($data['national_number'])) {
|
||||
$baseParts[] = $data['national_number'];
|
||||
} elseif (!empty($data['birthdate'])) {
|
||||
$year = substr($data['birthdate'], 0, 4);
|
||||
if (preg_match('/^\d{4}$/', $year)) {
|
||||
$baseParts[] = $year;
|
||||
}
|
||||
}
|
||||
$baseString = implode('|', $baseParts);
|
||||
$rawSecret = hash_hmac('sha256', $baseString, $pepper, true);
|
||||
$pwdHashed = password_hash($rawSecret, PASSWORD_DEFAULT);
|
||||
|
||||
/* ================== 4) Encrypt sensitive fields ================== */
|
||||
// فهارس البحث تُحسب من القيم الخام قبل التشفير — بعده تصبح القيمة الأصلية
|
||||
// غير متاحة، وبعد الانتقال إلى GCM لا يمكن استنتاجها من النص المشفّر.
|
||||
global $blindIndex;
|
||||
$phoneBidx = $blindIndex ? $blindIndex->index('driver.phone', $data['phone'] ?? null) : null;
|
||||
$emailBidx = $blindIndex ? $blindIndex->index('driver.email', $data['email'] ?? null) : null;
|
||||
$nameBidx = $blindIndex ? $blindIndex->index(
|
||||
'driver.name',
|
||||
trim(($data['first_name'] ?? '') . ' ' . ($data['last_name'] ?? ''))
|
||||
) : null;
|
||||
// مفتاح ربط جداول التحقق — يجب أن يطابق otpPhoneKey() حرفياً
|
||||
$phoneKey = otpPhoneKey($data['phone'] ?? null);
|
||||
|
||||
$toEncryptDriver = [
|
||||
"phone","email","first_name","last_name","name_arabic","gender",
|
||||
"national_number","address","site","fullNameMaritial","birthdate"
|
||||
];
|
||||
foreach ($toEncryptDriver as $f) {
|
||||
if (!empty($data[$f])) {
|
||||
$data[$f] = $encryptionHelper->encryptData($data[$f]);
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt car sensitive data
|
||||
$car['vin'] = $encryptionHelper->encryptData($car['vin']);
|
||||
$car['car_plate'] = $encryptionHelper->encryptData($car['car_plate']);
|
||||
$car['owner'] = $encryptionHelper->encryptData($car['owner']);
|
||||
|
||||
/* ================== 5) Start transaction ================== */
|
||||
$con->beginTransaction();
|
||||
|
||||
/* ================== 6) Check duplicate ================== */
|
||||
$dup = $con->prepare(
|
||||
"SELECT id FROM driver
|
||||
WHERE phone = :p OR email = :e
|
||||
OR (:pb IS NOT NULL AND phone_bidx = :pb)
|
||||
OR (:eb IS NOT NULL AND email_bidx = :eb)"
|
||||
);
|
||||
$dup->execute([
|
||||
':p' => $data['phone'],
|
||||
':e' => $data['email'],
|
||||
':pb' => $phoneBidx,
|
||||
':eb' => $emailBidx,
|
||||
]);
|
||||
if ($dup->rowCount() > 0) {
|
||||
$con->rollBack();
|
||||
jsonError("Phone or email already registered.");
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ================== 7) Insert Driver ================== */
|
||||
$sqlDriver = "
|
||||
INSERT INTO driver (
|
||||
id, phone, email, password, gender, license_type, national_number,
|
||||
name_arabic, issue_date, expiry_date, license_categories,
|
||||
address, licenseIssueDate, status, birthdate, site,
|
||||
first_name, last_name, accountBank, bankCode,
|
||||
employmentType, ai_data, user_input, maritalStatus,
|
||||
fullNameMaritial, expirationDate, created_at, updated_at,
|
||||
phone_bidx, email_bidx, name_bidx, phone_key
|
||||
) VALUES (
|
||||
:id, :phone, :email, :pwd, :gender, :license_type, :national_number,
|
||||
:name_arabic, :issue_date, :expiry_date, :license_categories,
|
||||
:address, :licenseIssueDate, :status, :birthdate, :site,
|
||||
:first_name, :last_name, :accountBank, :bankCode,
|
||||
:employmentType, :ai_data, :user_input, :maritalStatus,
|
||||
:fullNameMaritial, :expirationDate, NOW(), NOW(),
|
||||
:phone_bidx, :email_bidx, :name_bidx, :phone_key
|
||||
)
|
||||
";
|
||||
$insD = $con->prepare($sqlDriver);
|
||||
$okD = $insD->execute([
|
||||
':id' => $data['id'],
|
||||
':phone' => $data['phone'],
|
||||
':email' => $data['email'],
|
||||
':pwd' => $pwdHashed,
|
||||
':gender' => !empty($data['gender']) ? $data['gender'] : 'Male',
|
||||
':license_type' => !empty($data['license_type']) ? $data['license_type'] : 'yet',
|
||||
':national_number' => $data['national_number'],
|
||||
':name_arabic' => $data['name_arabic'],
|
||||
':issue_date' => !empty($data['issue_date']) ? $data['issue_date'] : '2020-01-01',
|
||||
':expiry_date' => !empty($data['expiry_date']) ? $data['expiry_date'] : 'yet',
|
||||
':license_categories' => !empty($data['license_categories']) ? $data['license_categories'] : 'B',
|
||||
':address' => $data['address'],
|
||||
':licenseIssueDate' => !empty($data['licenseIssueDate']) ? $data['licenseIssueDate'] : '2020-01-01',
|
||||
':status' => 'pending_review',
|
||||
':birthdate' => $data['birthdate'],
|
||||
':site' => !empty($data['site']) ? $data['site'] : 'demascus',
|
||||
':first_name' => $data['first_name'],
|
||||
':last_name' => $data['last_name'],
|
||||
':accountBank' => 'yet',
|
||||
':bankCode' => 'yet',
|
||||
':employmentType' => !empty($data['employmentType']) ? $data['employmentType'] : 'yet',
|
||||
':ai_data' => $aiRawText ?: null,
|
||||
':user_input' => $userInputJson ?: null,
|
||||
':maritalStatus' => !empty($data['maritalStatus']) ? $data['maritalStatus'] : 'yet',
|
||||
':fullNameMaritial' => !empty($data['fullNameMaritial']) ? $data['fullNameMaritial'] : 'yet',
|
||||
':expirationDate' => !empty($data['expirationDate']) ? $data['expirationDate'] : 'yet',
|
||||
':phone_bidx' => $phoneBidx,
|
||||
':email_bidx' => $emailBidx,
|
||||
':name_bidx' => $nameBidx,
|
||||
':phone_key' => $phoneKey,
|
||||
]);
|
||||
if (!$okD) {
|
||||
$con->rollBack();
|
||||
jsonError("Failed to insert driver.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$driverID = $data['id'];
|
||||
|
||||
/* ================== 8) Insert Vehicle ================== */
|
||||
// ✅ استقبال القيم الجديدة (التصنيف والوقود) مع تعيين افتراضي 1
|
||||
$vCatID = filterRequest("vehicle_category_id");
|
||||
$vCatID = ($vCatID !== null && $vCatID !== '') ? $vCatID : 1; // 1 = Car
|
||||
|
||||
$fTypeID = filterRequest("fuel_type_id");
|
||||
$fTypeID = ($fTypeID !== null && $fTypeID !== '') ? $fTypeID : 1; // 1 = Petrol
|
||||
|
||||
$hasCar = $con->prepare("SELECT 1 FROM CarRegistration WHERE driverID = :d LIMIT 1");
|
||||
$hasCar->execute([':d' => $driverID]);
|
||||
$isDefault = $hasCar->rowCount() === 0 ? 1 : 0;
|
||||
|
||||
$sqlCar = "
|
||||
INSERT INTO CarRegistration (
|
||||
driverID, vin, car_plate, make, model, year, expiration_date,
|
||||
color, owner, color_hex, fuel,
|
||||
vehicle_category_id, fuel_type_id,
|
||||
isDefault, created_at, status
|
||||
) VALUES (
|
||||
:driverID, :vin, :car_plate, :make, :model, :year, :expiration_date,
|
||||
:color, :owner, :color_hex, :fuel,
|
||||
:vehicle_category_id, :fuel_type_id,
|
||||
:isDefault, NOW(), 'yet'
|
||||
)
|
||||
";
|
||||
$insC = $con->prepare($sqlCar);
|
||||
$okC = $insC->execute([
|
||||
':driverID' => $driverID,
|
||||
':vin' => $car['vin'],
|
||||
':car_plate' => $car['car_plate'],
|
||||
':make' => $car['make'],
|
||||
':model' => $car['model'],
|
||||
':year' => $car['year'],
|
||||
':expiration_date' => $car['expiration_date'],
|
||||
':color' => $car['color'],
|
||||
':owner' => $car['owner'],
|
||||
':color_hex' => $car['color_hex'],
|
||||
':fuel' => $car['fuel'], // النص القديم (للتوافق)
|
||||
':vehicle_category_id' => $vCatID, // ✅ العمود الجديد
|
||||
':fuel_type_id' => $fTypeID, // ✅ العمود الجديد
|
||||
':isDefault' => $isDefault,
|
||||
]);
|
||||
if (!$okC) {
|
||||
$con->rollBack();
|
||||
jsonError("Failed to insert car registration.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$carRegID = $con->lastInsertId();
|
||||
|
||||
/* ================== 9) Store document links ================== */
|
||||
$insDoc = $con->prepare("
|
||||
INSERT INTO driver_documents (driverID, doc_type, image_name, link, upload_date)
|
||||
VALUES (:driverID, :doc_type, :image_name, :link, NOW())
|
||||
");
|
||||
|
||||
foreach ($docKeys as $k) {
|
||||
if (!isset($docUrls[$k])) continue;
|
||||
$url = $docUrls[$k];
|
||||
$name = basename(parse_url($url, PHP_URL_PATH) ?? '');
|
||||
if ($name === '') { $name = $k . '_' . time() . '.jpg'; }
|
||||
|
||||
$insDoc->execute([
|
||||
':driverID' => $driverID,
|
||||
':doc_type' => $k,
|
||||
':image_name' => $name,
|
||||
':link' => $url,
|
||||
]);
|
||||
|
||||
if ($k === 'profile_picture') {
|
||||
$insProfile = $con->prepare("
|
||||
INSERT INTO imageProfileCaptain (driverID, image_name, link)
|
||||
VALUES (:driverID, :image_name, :link)
|
||||
");
|
||||
$insProfile->execute([
|
||||
':driverID' => $driverID,
|
||||
':image_name' => $name,
|
||||
':link' => $url,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================== 10) Commit ================== */
|
||||
$con->commit();
|
||||
|
||||
/* ================== 11) Notification ================== */
|
||||
try {
|
||||
$fcmSendUrl = getenv('FCM_ENDPOINT_URL') ?: 'http://nginx/backend/ride/firebase/send_fcm.php';
|
||||
|
||||
$driverFullName = $raw_first_name . ' ' . $raw_last_name;
|
||||
$notificationTitle = 'تسجيل سائق جديد';
|
||||
$notificationBody = "سائق جديد ($driverFullName) سجل برقم ID: $driverID وهو بانتظار المراجعة والتفعيل.";
|
||||
|
||||
$notificationPayload = json_encode([
|
||||
'target' => 'service',
|
||||
'title' => $notificationTitle,
|
||||
'body' => $notificationBody,
|
||||
'isTopic' => true,
|
||||
'category' => 'new_driver_registration'
|
||||
]);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $fcmSendUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=UTF-8']);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $notificationPayload);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
|
||||
curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
} catch (Exception $notifyEx) {
|
||||
error_log("register_driver_and_car NOTIFY ERROR: " . $notifyEx->getMessage());
|
||||
}
|
||||
|
||||
printSuccess([
|
||||
'status' => 'success',
|
||||
'driverID' => $driverID,
|
||||
'carRegID' => $carRegID,
|
||||
'documents' => $docUrls
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if (isset($con) && $con instanceof PDO && $con->inTransaction()) {
|
||||
$con->rollBack();
|
||||
}
|
||||
$msg = $e->getMessage();
|
||||
error_log("register_driver_and_car ERROR: " . $msg);
|
||||
jsonError("Server error: $msg", 400);
|
||||
} catch (PDOException $e) {
|
||||
if (isset($con) && $con instanceof PDO && $con->inTransaction()) {
|
||||
$con->rollBack();
|
||||
}
|
||||
error_log("register_driver_and_car PDO: " . $e->getMessage());
|
||||
jsonError("Database error: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$id = filterRequest("id");
|
||||
|
||||
$sql = "DELETE FROM `passengers` WHERE `id` = :id";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess(null, "Passenger deleted successfully.");
|
||||
} else {
|
||||
jsonError("Failed to delete passenger.");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
// File: send_otp_driver.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 Driver] 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) تشفير البيانات وحفظها في DB ----------------------------------- */
|
||||
$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 {
|
||||
// حذف رموز قديمة
|
||||
$con->prepare("DELETE FROM token_verification_driver WHERE phone_number = ?")
|
||||
->execute([$receiver_enc]);
|
||||
|
||||
$stmt = $con->prepare("
|
||||
INSERT INTO token_verification_driver
|
||||
(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 compatibility
|
||||
if ($redis) {
|
||||
$redis->setex("otp:driver:$receiver", 300, $otp);
|
||||
}
|
||||
|
||||
jsonSuccess(null, 'OTP sent and saved successfully');
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[send_otp_driver.php] " . $e->getMessage());
|
||||
jsonError('OTP sent but failed to save to database');
|
||||
}
|
||||
|
||||
} else {
|
||||
jsonError('Failed to send OTP');
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../connect.php';
|
||||
|
||||
$phoneNumber = filterRequest("phone_number");
|
||||
$otp = filterRequest("otp");
|
||||
|
||||
if (empty($phoneNumber) || empty($otp)) {
|
||||
jsonError("Phone number and OTP are required.");
|
||||
exit();
|
||||
}
|
||||
|
||||
$phoneNumber_encrypted = otpPhoneKey($phoneNumber);
|
||||
// الرمز يُقارن بالتساوي أيضاً، فيحتاج نفس الصيغة الثابتة
|
||||
$otp_encrypted = otpPhoneKey($otp);
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare("
|
||||
SELECT * FROM token_verification_driver
|
||||
WHERE phone_number = ? AND token = ?
|
||||
");
|
||||
$stmt->execute([$phoneNumber_encrypted, $otp_encrypted]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result) {
|
||||
$expiration_time = strtotime($result['expiration_time']);
|
||||
|
||||
if (time() <= $expiration_time) {
|
||||
$con->prepare("UPDATE token_verification_driver SET verified = 1 WHERE id = ?")
|
||||
->execute([$result['id']]);
|
||||
|
||||
$driverStmt = $con->prepare("SELECT id FROM driver WHERE phone = ?");
|
||||
$driverStmt->execute([$phoneNumber_encrypted]);
|
||||
$driver = $driverStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($driver) {
|
||||
$driverID = $driver['id'];
|
||||
$newToken = filterRequest("token");
|
||||
$fingerPrint = filterRequest("fingerPrint");
|
||||
|
||||
if ($newToken && $fingerPrint) {
|
||||
$tokenEncrypted = $encryptionHelper->encryptData($newToken);
|
||||
|
||||
$checkTokenStmt = $con->prepare("SELECT id FROM driverToken WHERE captain_id = ?");
|
||||
$checkTokenStmt->execute([$driverID]);
|
||||
|
||||
if ($checkTokenStmt->rowCount() > 0) {
|
||||
$con->prepare("UPDATE driverToken SET token = ?, fingerPrint = ? WHERE captain_id = ?")
|
||||
->execute([$tokenEncrypted, $fingerPrint, $driverID]);
|
||||
} else {
|
||||
$con->prepare("INSERT INTO driverToken (token, fingerPrint, captain_id, created_at) VALUES (?, ?, ?, NOW())")
|
||||
->execute([$tokenEncrypted, $fingerPrint, $driverID]);
|
||||
}
|
||||
|
||||
$response = [
|
||||
"message" => "Driver token verified and updated.",
|
||||
"isRegistered" => true,
|
||||
"driverID" => $driverID
|
||||
];
|
||||
jsonSuccess($response);
|
||||
|
||||
} else {
|
||||
jsonError("Token or fingerprint missing.");
|
||||
}
|
||||
|
||||
} else {
|
||||
printSuccess([
|
||||
"message" => "Phone verified, but driver not found.",
|
||||
"isRegistered" => false
|
||||
]);
|
||||
}
|
||||
|
||||
} else {
|
||||
jsonError("OTP expired. Request a new one.");
|
||||
}
|
||||
|
||||
} else {
|
||||
jsonError("Invalid OTP.");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[verify_otp_driver.php] " . $e->getMessage());
|
||||
jsonError("Database error occurred.");
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$id = filterRequest("id");
|
||||
$columnValues = [];
|
||||
$params = [':id' => $id];
|
||||
|
||||
// الحقول التي تحتاج تشفير
|
||||
$fieldsToEncrypt = [
|
||||
"phone", "email", "gender", "birthdate", "site",
|
||||
"first_name", "last_name", "accountBank", "education",
|
||||
"employmentType", "maritalStatus"
|
||||
];
|
||||
|
||||
// الحقول غير المشفرة
|
||||
$plainFields = ["status", "bankCode", "updated_at"];
|
||||
|
||||
foreach ($_POST as $key => $value) {
|
||||
$filtered = filterRequest($key);
|
||||
|
||||
if ($key === "password") {
|
||||
// هاش لكلمة المرور
|
||||
$hashed = password_hash($filtered, PASSWORD_DEFAULT);
|
||||
$columnValues[] = "`password` = :password";
|
||||
$params[':password'] = $hashed;
|
||||
} elseif (in_array($key, $fieldsToEncrypt)) {
|
||||
$encrypted = $encryptionHelper->encryptData($filtered);
|
||||
$columnValues[] = "`$key` = :$key";
|
||||
$params[":$key"] = $encrypted;
|
||||
} elseif (in_array($key, $plainFields)) {
|
||||
$columnValues[] = "`$key` = :$key";
|
||||
$params[":$key"] = $filtered;
|
||||
}
|
||||
}
|
||||
|
||||
// بناء جملة التحديث
|
||||
if (empty($columnValues)) {
|
||||
jsonError("No data provided to update.");
|
||||
exit;
|
||||
}
|
||||
|
||||
$setClause = implode(", ", $columnValues);
|
||||
$sql = "UPDATE `driver` SET $setClause WHERE `id` = :id";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess(null, "Driver data updated successfully");
|
||||
} else {
|
||||
jsonError("Failed to update driver data");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// Sanitize and validate input
|
||||
$driverId = filterRequest("driverId");
|
||||
|
||||
// SQL query to check if a gift already exists for the driver (unclaimed)
|
||||
$checkSql = "SELECT COUNT(*) FROM driver_gifts WHERE driver_id = :driverId -- AND is_claimed = 0";
|
||||
|
||||
try {
|
||||
$checkStmt = $con->prepare($checkSql);
|
||||
$checkStmt->bindParam(':driverId', $driverId, PDO::PARAM_INT);
|
||||
$checkStmt->execute();
|
||||
$giftExists = $checkStmt->fetchColumn();
|
||||
|
||||
if ($giftExists > 0) {
|
||||
jsonError("Gift already exists for this driver");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Insert a new claimed gift
|
||||
$sql = "INSERT INTO driver_gifts (driver_id, gift_description, is_claimed)
|
||||
VALUES (:driverId, 'new account 300 le', 1)";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':driverId', $driverId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
jsonSuccess(null, "Gift data saved successfully");
|
||||
} else {
|
||||
jsonError("Failed to save gift data");
|
||||
}
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database Error: " . $e->getMessage());
|
||||
jsonError("An error occurred while saving the data");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// استقبال معرف السائق
|
||||
$id = filterRequest("id");
|
||||
|
||||
// استقبال بيانات شام كاش من التطبيق
|
||||
$accountBank = filterRequest("accountBank"); // الاسم (مثال: siro)
|
||||
$bankCode = filterRequest("bankCode"); // الكود الطويل (مثال: 80f23afe...)
|
||||
|
||||
// التحقق من وصول البيانات المطلوبة
|
||||
if ($id && $accountBank && $bankCode) {
|
||||
|
||||
try {
|
||||
// 1. تشفير اسم الحساب (حسب القواعد في السكربت السابق accountBank مشفر)
|
||||
$encryptedAccountBank = $encryptionHelper->encryptData($accountBank);
|
||||
|
||||
// 2. كود المحفظة يبقى كما هو (حسب القواعد bankCode غير مشفر)
|
||||
$plainBankCode = $encryptionHelper->encryptData($bankCode);
|
||||
|
||||
// 3. جملة التحديث
|
||||
$stmt = $con->prepare("UPDATE `driver` SET `accountBank` = ?, `bankCode` = ? WHERE `id` = ?");
|
||||
|
||||
$stmt->execute(array($encryptedAccountBank, $plainBankCode, $id));
|
||||
|
||||
// التحقق من نجاح العملية
|
||||
// rowCount > 0 يعني تم التحديث، أحياناً يعطي 0 إذا كانت البيانات هي نفسها لم تتغير
|
||||
// لذا نرسل نجاح في كلتا الحالتين طالما لم يحدث Error
|
||||
jsonSuccess(null, "ShamCash info updated successfully");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
// في حال وجود خطأ في قاعدة البيانات
|
||||
error_log("[updateShamCashDriver] " . $e->getMessage());
|
||||
jsonError("Database Error");
|
||||
}
|
||||
|
||||
} else {
|
||||
jsonError("Missing required fields: id, accountBank, or bankCode");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
// File: upload_serial_document.php
|
||||
// يرفع صورة وثيقة إلى مسار خاص (خارج الويب العام) ويُرجع Signed URL مؤقّت.
|
||||
|
||||
// بيئتك
|
||||
require_once __DIR__ . '/../../connect.php'; // يجب أن يوفّر: $con (اختياري) + printSuccess/printFailure + filterRequest
|
||||
|
||||
// --------- إعدادات ---------
|
||||
const MAX_FILE_MB = 5;
|
||||
const ALLOWED_MIMES = ['image/jpeg','image/png','image/webp']; // فقط صور
|
||||
const UPLOAD_ROOT = __DIR__ . "/../../private_uploads"; // مجلد خاص (غير عام)
|
||||
$SIGN_SECRET = getenv('SECRET_KEY_HMAC'); // غيّرها واقرأها من .env
|
||||
$host = getenv('APP_DOMAIN');
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
define('PUBLIC_BASE', "$protocol://$host/siro");
|
||||
const SIGNED_TTL_SEC = 172800; // 2 days = 60*60*24
|
||||
|
||||
// أنشئ مجلد الرفع إن لم يكن موجودًا
|
||||
if (!is_dir(UPLOAD_ROOT)) { @mkdir(UPLOAD_ROOT, 0700, true); }
|
||||
|
||||
// Log entry
|
||||
uploadLog("🚀 [uploadSyrianDocs.php] Document upload script started.");
|
||||
|
||||
// (اختياري) هيدرز أمان
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
$hmacHeader = $_SERVER['HTTP_X_HMAC_AUTH'] ?? '';
|
||||
// TODO: تحقّق حسب منطقك إن أردت فرض المصادقة هنا.
|
||||
|
||||
// --------- حقول مطلوبة من Flutter عبر filterRequest ---------
|
||||
$driverId = filterRequest('driver_id');
|
||||
$docType = filterRequest('doc_type');
|
||||
$purpose = filterRequest('purpose'); // اختياري
|
||||
|
||||
uploadLog("📥 Request params: driver_id=$driverId, doc_type=$docType");
|
||||
|
||||
if (empty($driverId) || empty($docType)) {
|
||||
uploadLog("❌ Missing driver_id or doc_type params.", 'ERROR');
|
||||
jsonError("driver_id and doc_type are required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// اسمح فقط بقيم محددة للوثائق
|
||||
$allowedDocTypes = [
|
||||
'driver_license_front',
|
||||
'driver_license_back',
|
||||
'car_license_front',
|
||||
'car_license_back',
|
||||
];
|
||||
if (!in_array($docType, $allowedDocTypes, true)) {
|
||||
uploadLog("❌ Invalid doc_type value: $docType", 'ERROR');
|
||||
jsonError("Invalid doc_type.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// --------- التحقق من الملف ---------
|
||||
if (isset($_FILES['file'])) {
|
||||
uploadLog('$_FILES[\'file\'] metadata', 'INFO', [
|
||||
'name' => $_FILES['file']['name'] ?? 'unknown',
|
||||
'type' => $_FILES['file']['type'] ?? 'unknown',
|
||||
'size' => $_FILES['file']['size'] ?? 0,
|
||||
'upload_error_code' => $_FILES['file']['error'] ?? UPLOAD_ERR_OK
|
||||
]);
|
||||
} else {
|
||||
uploadLog("No 'file' payload was sent in the request.", 'WARNING');
|
||||
}
|
||||
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
$err = $_FILES['file']['error'] ?? 'missing_file';
|
||||
uploadLog("❌ File upload validation failed. Code: $err", 'ERROR');
|
||||
jsonError("No file uploaded or upload error.");
|
||||
exit;
|
||||
}
|
||||
$tmpPath = $_FILES['file']['tmp_name'];
|
||||
$origName = $_FILES['file']['name'] ?? 'upload.bin';
|
||||
$size = filesize($tmpPath);
|
||||
if ($size === false || $size <= 0) {
|
||||
jsonError("Invalid file size."); exit;
|
||||
}
|
||||
if ($size > MAX_FILE_MB * 1024 * 1024) {
|
||||
jsonError("File too large. Max " . MAX_FILE_MB . " MB."); exit;
|
||||
}
|
||||
|
||||
// MIME دقيق
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($tmpPath) ?: 'application/octet-stream';
|
||||
if (!in_array($mime, ALLOWED_MIMES, true)) {
|
||||
jsonError("Unsupported file type: $mime"); exit;
|
||||
}
|
||||
|
||||
// لاحقة الامتداد
|
||||
$extMap = [
|
||||
'image/jpeg' => '.jpg',
|
||||
'image/png' => '.png',
|
||||
'image/webp' => '.webp',
|
||||
];
|
||||
$ext = $extMap[$mime];
|
||||
|
||||
// --------- توليد مسار حتمي بدون تاريخ ---------
|
||||
// تنظيف driver_id لاسم ملف آمن
|
||||
$driverIdSafe = preg_replace('/[^A-Za-z0-9_\-]/', '_', $driverId);
|
||||
// شجرة مجلدات ثابتة من hash(driver_id) لتوزيع الملفات
|
||||
$h = hash('sha1', $driverIdSafe);
|
||||
$subdir = substr($h, 0, 2) . '/' . substr($h, 2, 2);
|
||||
$destDir = UPLOAD_ROOT . '/' . $subdir;
|
||||
if (!is_dir($destDir)) { @mkdir($destDir, 0700, true); }
|
||||
|
||||
// الاسم النهائي بدون تاريخ
|
||||
$serverName = "{$driverIdSafe}__{$docType}{$ext}";
|
||||
$destPath = $destDir . '/' . $serverName;
|
||||
|
||||
// استبدال أي نسخة قديمة عن قصد (overwrite) - مع حماية ضد path traversal
|
||||
$resolvedDest = realpath($destPath) ?: $destPath;
|
||||
$resolvedRoot = realpath(UPLOAD_ROOT) ?: UPLOAD_ROOT;
|
||||
|
||||
if (is_file($destPath) && str_starts_with($resolvedDest, $resolvedRoot)) {
|
||||
@unlink($destPath);
|
||||
}
|
||||
|
||||
// نقل الملف
|
||||
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||
jsonError("Failed to save the uploaded file.");
|
||||
exit;
|
||||
}
|
||||
@chmod($destPath, 0600);
|
||||
|
||||
// --------- Signed URL ---------
|
||||
// سنضمّن driver_id و doc_type و ext في الرابط والتوقيع.
|
||||
// ext بدون النقطة
|
||||
$extShort = ltrim($ext, '.');
|
||||
$expires = time() + SIGNED_TTL_SEC;
|
||||
|
||||
// الرسالة الموقّعة: driver_id:doc_type:ext:expires
|
||||
$message = $driverIdSafe . ':' . $docType . ':' . $extShort . ':' . $expires;
|
||||
$signature = hash_hmac('sha256', $message, SIGN_SECRET);
|
||||
|
||||
// رابط القراءة عبر البوابة الآمنة فقط
|
||||
// ملاحظة: لا نُرجع المسار الحقيقي، فقط معطيات موقّعة
|
||||
$fileUrl = PUBLIC_BASE . "/secure_image.php"
|
||||
. "?driver_id={$driverIdSafe}"
|
||||
. "&doc_type={$docType}"
|
||||
. "&ext={$extShort}"
|
||||
. "&expires={$expires}"
|
||||
. "&signature={$signature}";
|
||||
|
||||
// --------- استجابة ---------
|
||||
uploadLog("✅ Document upload succeeded. URL: $fileUrl");
|
||||
printSuccess([
|
||||
"status" => "success",
|
||||
"success_file" => true,
|
||||
"file_url" => $fileUrl,
|
||||
"file_name" => $serverName, // الاسم الفعلي المحفوظ
|
||||
"driver_id" => $driverIdSafe,
|
||||
"doc_type" => $docType,
|
||||
"mime_type" => $mime,
|
||||
"size_bytes" => $size,
|
||||
"expires_at" => date('c', $expires)
|
||||
]);
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// auth/syria/uploadImage.php
|
||||
// رفع صور وثائق السائق (هوية، رخصة، صورة شخصية، ...)
|
||||
// يخزّنها في مجلدات حسب الدولة: auth/uploads/{country}/
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
uploadLog("🚀 [uploadImage.php] Document upload started.");
|
||||
|
||||
// --------- قراءة الحقول ---------
|
||||
$driverID = filterRequest('driverID');
|
||||
$imageType = filterRequest('imageType');
|
||||
$country = filterRequest('country');
|
||||
|
||||
if (empty($imageType)) {
|
||||
jsonError('imageType is required.');
|
||||
}
|
||||
|
||||
// --------- تحديد الدولة ---------
|
||||
$country = strtolower(trim($country ?: ''));
|
||||
if (empty($country)) {
|
||||
if (!empty($driverID)) {
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT country FROM drivers WHERE id = ?");
|
||||
$stmt->execute([$driverID]);
|
||||
$country = strtolower(trim((string)$stmt->fetchColumn()));
|
||||
} catch (Exception $e) {
|
||||
$country = '';
|
||||
}
|
||||
}
|
||||
if (empty($country)) {
|
||||
$country = 'jordan';
|
||||
}
|
||||
}
|
||||
if (!in_array($country, ['syria', 'jordan', 'egypt'])) {
|
||||
$country = 'jordan';
|
||||
}
|
||||
|
||||
// --------- بادئة اسم الملف ---------
|
||||
$prefix = !empty($driverID) ? $driverID : 'unregistered';
|
||||
|
||||
uploadLog("📥 Params: driverID=" . ($driverID ?: 'null') . ", imageType=$imageType, country=$country");
|
||||
|
||||
// --------- رفع الملف ---------
|
||||
$targetDir = __DIR__ . "/../../auth/uploads/{$country}/";
|
||||
$result = uploadImageSecure('image', $targetDir, $prefix . '_' . $imageType);
|
||||
|
||||
if (!$result['success']) {
|
||||
uploadLog("❌ Upload failed: {$result['error']}", 'ERROR', [
|
||||
'driverID' => $driverID,
|
||||
'imageType' => $imageType,
|
||||
'country' => $country,
|
||||
]);
|
||||
jsonError($result['error']);
|
||||
}
|
||||
|
||||
// --------- إزالة العشوائية من اسم الملف ---------
|
||||
// الاسم يكون فقط: {driverID}_{imageType}.jpg (بدون random hex)
|
||||
$ext = pathinfo($result['filename'], PATHINFO_EXTENSION);
|
||||
$simpleName = $prefix . '_' . $imageType . '.' . $ext;
|
||||
$simplePath = rtrim($targetDir, '/') . '/' . $simpleName;
|
||||
|
||||
if (file_exists($simplePath)) {
|
||||
unlink($simplePath); // overwrite
|
||||
}
|
||||
rename($result['path'], $simplePath);
|
||||
|
||||
$result['filename'] = $simpleName;
|
||||
$result['path'] = $simplePath;
|
||||
|
||||
// --------- بناء الرابط العام ---------
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? (getenv('APP_DOMAIN') ?: 'api.siromove.com');
|
||||
|
||||
$basePath = rtrim(dirname(dirname(dirname($_SERVER['SCRIPT_NAME']))), '/');
|
||||
$url = "$protocol://$host{$basePath}/auth/uploads/{$country}/{$result['filename']}";
|
||||
|
||||
uploadLog("✅ Uploaded: {$result['path']} -> $url", 'INFO', [
|
||||
'driverID' => ($driverID ?: 'null'),
|
||||
'imageType' => $imageType,
|
||||
'country' => $country,
|
||||
]);
|
||||
|
||||
jsonSuccess([
|
||||
'url' => $url,
|
||||
'file_link' => $url,
|
||||
'filename' => $result['filename'],
|
||||
'driverID' => ($driverID ?: ''),
|
||||
'imageType' => $imageType,
|
||||
'country' => $country,
|
||||
]);
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$email = filterRequest('email');
|
||||
$phone = filterRequest('phone');
|
||||
$password = filterRequest('password');
|
||||
|
||||
if (empty($phone) && empty($email)) {
|
||||
echo json_encode(["status" => "Failure", "data" => "Phone or email is required."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* البحث عن الحساب.
|
||||
*
|
||||
* سابقاً كان يقارن القيمة الخام بالعمود المشفّر مباشرةً، وهو ما ينجح فقط لأن
|
||||
* التشفير الحالي حتمي (CBC بـ IV ثابت). الفهرس الأعمى يجعل هذا الاستعلام
|
||||
* مستقلاً عن أسلوب التشفير، فلا ينكسر تسجيل الدخول عند الانتقال إلى AES-GCM.
|
||||
*
|
||||
* تُبقى المقارنتان القديمتان في نفس الشرط كاحتياط للحسابات التي لم تُفهرس بعد.
|
||||
*/
|
||||
global $blindIndex;
|
||||
|
||||
$conditions = [];
|
||||
$params = [':password' => $password];
|
||||
|
||||
if (!empty($phone)) {
|
||||
$conditions[] = "passengers.phone = :phone";
|
||||
$params[':phone'] = $phone;
|
||||
|
||||
$phoneBidx = $blindIndex ? $blindIndex->index('passengers.phone', $phone) : null;
|
||||
if ($phoneBidx) {
|
||||
$conditions[] = "passengers.phone_bidx = :phone_bidx";
|
||||
$params[':phone_bidx'] = $phoneBidx;
|
||||
}
|
||||
}
|
||||
if (!empty($email)) {
|
||||
$conditions[] = "passengers.email = :email";
|
||||
$params[':email'] = $email;
|
||||
|
||||
$emailBidx = $blindIndex ? $blindIndex->index('passengers.email', $email) : null;
|
||||
if ($emailBidx) {
|
||||
$conditions[] = "passengers.email_bidx = :email_bidx";
|
||||
$params[':email_bidx'] = $emailBidx;
|
||||
}
|
||||
}
|
||||
$where = implode(' OR ', $conditions);
|
||||
|
||||
$sql = "SELECT
|
||||
passengers.`id`,
|
||||
passengers.`phone`,
|
||||
passengers.`email`,
|
||||
passengers.`password`,
|
||||
passengers.`gender`,
|
||||
passengers.`birthdate`,
|
||||
passengers.`site`,
|
||||
passengers.`first_name`,
|
||||
passengers.`last_name`,
|
||||
passengers.`education`,
|
||||
passengers.`employmentType`,
|
||||
passengers.`maritalStatus`,
|
||||
passengers.`created_at`,
|
||||
passengers.`updated_at`,
|
||||
passengers.`email` AS `_email_enc`
|
||||
FROM
|
||||
`passengers`
|
||||
WHERE
|
||||
$where";
|
||||
|
||||
/**
|
||||
* حالة توثيق البريد.
|
||||
*
|
||||
* كان الاستعلام يربط email_verifications.email بعمود البريد في الحساب، لكن
|
||||
* الأول يُخزَّن نصاً صريحاً والثاني مشفّراً — فالربط لم يكن يطابق شيئاً أصلاً
|
||||
* وكانت verified تعود NULL دائماً. نجلبها هنا بالبريد الأصلي.
|
||||
*/
|
||||
function fetchEmailVerified(PDO $con, ?string $plainEmail): ?int
|
||||
{
|
||||
if (!$plainEmail) return null;
|
||||
try {
|
||||
$st = $con->prepare("SELECT verified FROM email_verifications WHERE email = ? LIMIT 1");
|
||||
$st->execute([$plainEmail]);
|
||||
$v = $st->fetchColumn();
|
||||
return $v === false ? null : (int) $v;
|
||||
} catch (PDOException $e) {
|
||||
error_log('[email_verifications] ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$count = $stmt->rowCount();
|
||||
|
||||
if ($count > 0) {
|
||||
$plainEmail = $encryptionHelper->decryptData($data[0]['_email_enc'] ?? null) ?: null;
|
||||
$data[0]['verified'] = fetchEmailVerified($con, $plainEmail);
|
||||
unset($data[0]['_email_enc']);
|
||||
|
||||
$stored_password = $data[0]['password'];
|
||||
if (password_verify($password, $stored_password)) {
|
||||
unset($data[0]['password']);
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
"count" => $count,
|
||||
"data" => $data
|
||||
]);
|
||||
} else {
|
||||
// The password is incorrect
|
||||
echo json_encode([
|
||||
"status" => "Failure",
|
||||
"data" => "Incorrect password."
|
||||
]);
|
||||
// jsonError("Incorrect password.");
|
||||
}
|
||||
} else {
|
||||
echo json_encode([
|
||||
"status" => "Failure",
|
||||
"data" => "Invalid credentials."
|
||||
]);
|
||||
}
|
||||
$con = null;
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,299 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?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);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
// File: backend/auth/otp/verify.php
|
||||
// Unified OTP verification endpoint
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
// 0. Rate Limiting: 3 محاولات OTP كل 5 دقائق لكل IP
|
||||
$rateLimiter = new RateLimiter($redis);
|
||||
$rateLimiter->enforce(RateLimiter::identifier(), 'otp_verify');
|
||||
|
||||
// 1. Fetch input parameters
|
||||
$phone_number = filterRequest("phone_number");
|
||||
if (empty($phone_number)) {
|
||||
$phone_number = filterRequest("receiver");
|
||||
}
|
||||
|
||||
$token_code = filterRequest("token_code");
|
||||
if (empty($token_code)) {
|
||||
$token_code = filterRequest("token");
|
||||
}
|
||||
|
||||
$user_type = filterRequest("user_type");
|
||||
$context = filterRequest("context"); // token_change | login (default)
|
||||
|
||||
// user_type is taken from request only (JWT not trusted without signature verification)
|
||||
|
||||
if (empty($phone_number)) {
|
||||
jsonError("Phone number is required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($token_code)) {
|
||||
jsonError("Verification token code is required.");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (empty($user_type)) {
|
||||
if (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;
|
||||
}
|
||||
|
||||
// 2. Establish DB Connection
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
exit(json_encode(['error' => 'Database connection failed']));
|
||||
}
|
||||
|
||||
// 3. Encrypt data to query
|
||||
// 4. Verify based on user type
|
||||
try {
|
||||
$encryptedPhoneSearch = otpPhoneKey($phone_number);
|
||||
|
||||
if ($user_type === 'admin') {
|
||||
$sql = "SELECT * FROM token_verification_admin
|
||||
WHERE expiration_time >= NOW() AND verified = 0 AND phone_number = ?";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$encryptedPhoneSearch]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$matchedRow = null;
|
||||
foreach ($rows as $row) {
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token']);
|
||||
if ($decryptedToken === $token_code) {
|
||||
$matchedRow = $row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRow) {
|
||||
$deviceNumber = filterRequest("device_number") ?? '';
|
||||
// adminUser stores unencrypted phone
|
||||
$checkAdmin = $con->prepare("SELECT * FROM adminUser WHERE name = ?");
|
||||
$checkAdmin->execute([$phone_number]);
|
||||
$now = date("Y-m-d H:i:s");
|
||||
|
||||
// Mark token as verified
|
||||
$updateToken = $con->prepare("UPDATE token_verification_admin SET verified = 1 WHERE phone_number = ? AND token = ?");
|
||||
$updateToken->execute([$matchedRow['phone_number'], $matchedRow['token']]);
|
||||
|
||||
if ($checkAdmin->rowCount() > 0) {
|
||||
$update = $con->prepare("UPDATE adminUser SET device_number = ?, updated_at = ? WHERE name = ?");
|
||||
$update->execute([$deviceNumber, $now, $phone_number]);
|
||||
jsonSuccess(["message" => "verified and updated existing admin"]);
|
||||
} else {
|
||||
$insert = $con->prepare("INSERT INTO adminUser (device_number, name, created_at, updated_at) VALUES (?, ?, ?, ?)");
|
||||
$insert->execute([$deviceNumber, $phone_number, $now, $now]);
|
||||
jsonSuccess(["message" => "verified and new admin created"]);
|
||||
}
|
||||
} else {
|
||||
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
|
||||
}
|
||||
} elseif ($user_type === 'service') {
|
||||
$sql = "SELECT `id`, `phone_number`, `token_code` FROM `phone_verification_service`
|
||||
WHERE `expiration_time` > NOW() AND `is_verified` = 0 AND `phone_number` = ?";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$encryptedPhoneSearch]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$matchedRowId = null;
|
||||
foreach ($rows as $row) {
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token_code']);
|
||||
if ($decryptedToken === $token_code) {
|
||||
$matchedRowId = $row['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRowId) {
|
||||
$sqlUpdate = "UPDATE `phone_verification_service` SET `is_verified` = 1 WHERE `id` = :id";
|
||||
$stmtUpd = $con->prepare($sqlUpdate);
|
||||
$stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT);
|
||||
$stmtUpd->execute();
|
||||
jsonSuccess(null, "Your phone number has been verified.");
|
||||
} else {
|
||||
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
|
||||
}
|
||||
} elseif ($user_type === 'driver') {
|
||||
if ($context === 'token_change') {
|
||||
$sql = "SELECT `id`, `phone_number`, `token` FROM `token_verification_driver`
|
||||
WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$encryptedPhoneSearch]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$matchedRowId = null;
|
||||
foreach ($rows as $row) {
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token']);
|
||||
if ($decryptedToken === $token_code) {
|
||||
$matchedRowId = $row['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRowId) {
|
||||
$sqlUpdate = "UPDATE `token_verification_driver` SET `verified` = 1 WHERE `id` = :id";
|
||||
$stmtUpd = $con->prepare($sqlUpdate);
|
||||
$stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT);
|
||||
$stmtUpd->execute();
|
||||
jsonSuccess(null, "Your phone number has been verified.");
|
||||
} else {
|
||||
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
|
||||
}
|
||||
} else {
|
||||
$sql = "SELECT `id`, `phone_number`, `token_code` FROM `phone_verification`
|
||||
WHERE `expiration_time` > NOW() AND `is_verified` = 0 AND `phone_number` = ?";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$encryptedPhoneSearch]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$matchedRowId = null;
|
||||
foreach ($rows as $row) {
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token_code']);
|
||||
if ($decryptedToken === $token_code) {
|
||||
$matchedRowId = $row['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRowId) {
|
||||
$sqlUpdate = "UPDATE `phone_verification` SET `is_verified` = 1 WHERE `id` = :id";
|
||||
$stmtUpd = $con->prepare($sqlUpdate);
|
||||
$stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT);
|
||||
$stmtUpd->execute();
|
||||
|
||||
// Check registration status
|
||||
$isRegistered = false;
|
||||
$driverData = null;
|
||||
|
||||
$chkStmt = $con->prepare("SELECT id, first_name, last_name, email, phone FROM driver WHERE phone = ?");
|
||||
$chkStmt->execute([$encryptionHelper->encryptData($phone_number)]);
|
||||
$driver = $chkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Generate driverID for unregistered users (hash of phone)
|
||||
$driverID = '';
|
||||
if ($driver) {
|
||||
$isRegistered = true;
|
||||
$driver['first_name'] = $encryptionHelper->decryptData($driver['first_name']);
|
||||
$driver['last_name'] = $encryptionHelper->decryptData($driver['last_name']);
|
||||
$driver['email'] = $encryptionHelper->decryptData($driver['email']);
|
||||
$driver['phone'] = $encryptionHelper->decryptData($driver['phone']);
|
||||
$driverData = $driver;
|
||||
$driverID = (string)$driver['id'];
|
||||
} else {
|
||||
// driverID ثابت ومشتق من رقم الهاتف (نفس الرقم = نفس الـ ID)
|
||||
$driverID = substr(md5($phone_number), 0, 16);
|
||||
}
|
||||
|
||||
// Generate JWT tokens for driver
|
||||
$audDriver = filterRequest("aud") ?: filterRequest("audience") ?: (getenv('allowedDriver2') ?: 'driver-app:ios');
|
||||
$fpDriver = filterRequest("fingerprint") ?? filterRequest("fingerPrint") ?? ($_SERVER['HTTP_X_DEVICE_FP'] ?? null);
|
||||
if ($fpDriver === null && function_exists('getallheaders')) {
|
||||
$hdrs = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$fpDriver = $hdrs['x-device-fp'] ?? null;
|
||||
}
|
||||
$jwtSvc = new JwtService($redis);
|
||||
$accessToken = $jwtSvc->generateAccessToken($driverID, 'driver', $audDriver, $fpDriver);
|
||||
$refreshToken = $jwtSvc->generateRefreshToken($driverID, 'driver', $audDriver);
|
||||
|
||||
jsonSuccess([
|
||||
"isRegistered" => $isRegistered,
|
||||
"driver" => $driverData,
|
||||
"driverID" => $driverID,
|
||||
"jwt" => $accessToken,
|
||||
"token" => $accessToken,
|
||||
"access_token" => $accessToken,
|
||||
"refresh_token" => $refreshToken
|
||||
], "Your phone number has been verified.");
|
||||
} else {
|
||||
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($context === 'token_change') {
|
||||
$sql = "SELECT `id`, `phone_number`, `token` FROM `token_verification`
|
||||
WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$encryptedPhoneSearch]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$matchedRowId = null;
|
||||
foreach ($rows as $row) {
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token']);
|
||||
if ($decryptedToken === $token_code) {
|
||||
$matchedRowId = $row['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRowId) {
|
||||
$sqlUpdate = "UPDATE `token_verification` SET `verified` = 1 WHERE `id` = :id";
|
||||
$stmtUpd = $con->prepare($sqlUpdate);
|
||||
$stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT);
|
||||
$stmtUpd->execute();
|
||||
jsonSuccess(null, "Your phone number has been verified.");
|
||||
} else {
|
||||
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
|
||||
}
|
||||
} else {
|
||||
$sql = "SELECT `id`, `phone_number`, `token` FROM `phone_verification_passenger`
|
||||
WHERE `expiration_time` > NOW() AND `verified` = 0 AND `phone_number` = ?";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([$encryptedPhoneSearch]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$matchedRowId = null;
|
||||
foreach ($rows as $row) {
|
||||
$decryptedToken = $encryptionHelper->decryptData($row['token']);
|
||||
if ($decryptedToken === $token_code) {
|
||||
$matchedRowId = $row['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRowId) {
|
||||
$sqlUpdate = "UPDATE `phone_verification_passenger` SET `verified` = 1 WHERE `id` = :id";
|
||||
$stmtUpd = $con->prepare($sqlUpdate);
|
||||
$stmtUpd->bindParam(':id', $matchedRowId, PDO::PARAM_INT);
|
||||
$stmtUpd->execute();
|
||||
|
||||
// Check registration status
|
||||
$isRegistered = false;
|
||||
$passengerData = null;
|
||||
$passengerID = '';
|
||||
|
||||
$chkStmt = $con->prepare("SELECT id, first_name, last_name, email, phone FROM passengers WHERE phone = ?");
|
||||
$chkStmt->execute([$encryptionHelper->encryptData($phone_number)]);
|
||||
$passenger = $chkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($passenger) {
|
||||
$isRegistered = true;
|
||||
$passenger['first_name'] = $encryptionHelper->decryptData($passenger['first_name']);
|
||||
$passenger['last_name'] = $encryptionHelper->decryptData($passenger['last_name']);
|
||||
$passenger['email'] = $encryptionHelper->decryptData($passenger['email']);
|
||||
$passenger['phone'] = $encryptionHelper->decryptData($passenger['phone']);
|
||||
$passengerData = $passenger;
|
||||
$passengerID = (string)$passenger['id'];
|
||||
} else {
|
||||
$passengerID = substr(md5($phone_number), 0, 16);
|
||||
}
|
||||
|
||||
// Generate JWT tokens for passenger
|
||||
$audPass = filterRequest("aud") ?: filterRequest("audience") ?: (getenv('allowed2') ?: 'passenger-app:ios');
|
||||
$fpPass = filterRequest("fingerprint") ?? filterRequest("fingerPrint") ?? ($_SERVER['HTTP_X_DEVICE_FP'] ?? null);
|
||||
if ($fpPass === null && function_exists('getallheaders')) {
|
||||
$hdrs = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$fpPass = $hdrs['x-device-fp'] ?? null;
|
||||
}
|
||||
$jwtSvc = new JwtService($redis);
|
||||
$accessToken = $jwtSvc->generateAccessToken($passengerID, 'passenger', $audPass, $fpPass);
|
||||
$refreshToken = $jwtSvc->generateRefreshToken($passengerID, 'passenger', $audPass);
|
||||
|
||||
jsonSuccess([
|
||||
"isRegistered" => $isRegistered,
|
||||
"passenger" => $passengerData,
|
||||
"jwt" => $accessToken,
|
||||
"token" => $accessToken,
|
||||
"access_token" => $accessToken,
|
||||
"refresh_token" => $refreshToken
|
||||
], "Your phone number has been verified.");
|
||||
} else {
|
||||
jsonError("Your phone number could not be verified or the code is expired. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("⚠️ [OTP DB Verify] Error: " . $e->getMessage());
|
||||
jsonError("An error occurred during verification. Please try again.");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$platform = filterRequest("platform");
|
||||
$appName = filterRequest("appName");
|
||||
|
||||
$sql = "SELECT
|
||||
`id`,
|
||||
`platform`,
|
||||
`appName`,
|
||||
`createdAt`,
|
||||
`version`
|
||||
FROM
|
||||
`packageInfo`
|
||||
WHERE
|
||||
platform='$platform' and appName='$appName';";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Print all the records
|
||||
// printData($result);
|
||||
jsonSuccess($data = $result);
|
||||
} else {
|
||||
// Print a failure message
|
||||
jsonError($message = "No records found");
|
||||
}
|
||||
?>
|
||||
@@ -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();
|
||||
@@ -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.");
|
||||
}
|
||||
?>
|
||||
@@ -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);
|
||||
|
||||
?>
|
||||
@@ -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);
|
||||
}
|
||||
?>
|
||||
@@ -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");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$email = filterRequest("email");
|
||||
$token = filterRequest("token");
|
||||
|
||||
$admin='support@siromove.com';
|
||||
$headers = "MIME-Version: 1.0" . "\r\n";
|
||||
$headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
|
||||
$headers .= "From: $admin" . "\r\n";
|
||||
|
||||
$subject = "Verify your email address";
|
||||
$bodyEmail = "
|
||||
<html>
|
||||
<head>
|
||||
<title>Verify your email address</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hi [$email],</p>
|
||||
|
||||
<p>We recently received a request to verify your email address for your account on Siro App.</p>
|
||||
|
||||
<p>To verify your email address, please write this to app .</p>
|
||||
$token
|
||||
|
||||
<p>If you did not request to verify your email address, please ignore this email.</p>
|
||||
|
||||
<p>Thank you,</p>
|
||||
Siro Team.
|
||||
</body>
|
||||
</html>
|
||||
";
|
||||
|
||||
mail($email, $subject, $bodyEmail, $headers);
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$email = filterRequest("email");
|
||||
$token = filterRequest("token");
|
||||
|
||||
$stmt = $con->prepare("SELECT * FROM `email_verifications` WHERE `email` = ?");
|
||||
$stmt->execute([$email]);
|
||||
|
||||
$rowCount = $stmt->rowCount();
|
||||
|
||||
$admin='support@mobile-app.store';
|
||||
$headers = "MIME-Version: 1.0" . "\r\n";
|
||||
$headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
|
||||
$headers .= "From: $admin" . "\r\n";
|
||||
|
||||
$subject = "Verify your email address";
|
||||
$bodyEmail = "
|
||||
<html>
|
||||
<head>
|
||||
<title>Verify your email address</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hi [$email],</p>
|
||||
|
||||
<p>We recently received a request to verify your email address for your account on SEFER App.</p>
|
||||
|
||||
<p>To verify your email address, please write this to app .</p>
|
||||
$token
|
||||
|
||||
<p>If you did not request to verify your email address, please ignore this email.</p>
|
||||
|
||||
<p>Thank you,</p>
|
||||
SEFER Team.
|
||||
</body>
|
||||
</html>
|
||||
";
|
||||
|
||||
|
||||
|
||||
if ($rowCount > 0) {
|
||||
// The email already exists, so update the data
|
||||
// كانت القيم تُدمج في نص الاستعلام مباشرةً — حقن SQL عبر البريد أو الرمز.
|
||||
$stmt = $con->prepare("UPDATE `email_verifications` SET `token` = ? WHERE `email` = ?");
|
||||
$stmt->execute([$token, $email]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// The update was successful
|
||||
jsonSuccess($message = "Email verification data updated successfully");
|
||||
mail($email, $subject, $bodyEmail, $headers);
|
||||
} else {
|
||||
// The update was unsuccessful
|
||||
jsonError($message = "Failed to update email verification data");
|
||||
}
|
||||
} else {
|
||||
// The email does not exist, so insert the data
|
||||
$stmt = $con->prepare("INSERT INTO `email_verifications` (`email`, `token`) VALUES (?, ?)");
|
||||
$stmt->execute([$email, $token]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// The insertion was successful
|
||||
jsonSuccess($message = "Email verification data saved successfully");
|
||||
mail($email, $subject, $bodyEmail, $headers);
|
||||
} else {
|
||||
// The insertion was unsuccessful
|
||||
jsonError($message = "Failed to save email verification data");
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
$email = filterRequest("email");
|
||||
$token = filterRequest("token");
|
||||
|
||||
$sql = "SELECT `id`, `email`, `token`, `created_at`, `updated_at`, `verified` FROM `email_verifications` WHERE `email` = :email AND `token` = :token";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([':email' => $email, ':token' => $token]);
|
||||
$result = $stmt->fetch();
|
||||
|
||||
if ($result) {
|
||||
$id = $result["id"];
|
||||
$sql = "UPDATE `email_verifications` SET `verified` = 1 WHERE `id` = :id";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute([':id' => $id]);
|
||||
|
||||
$admin='support@siromove.com';
|
||||
$headers = "MIME-Version: 1.0" . "\r\n";
|
||||
$headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
|
||||
$headers .= "From: $admin" . "\r\n";
|
||||
|
||||
$subject = " Verify your email address";
|
||||
$bodyEmail="Subject: Verify your email address
|
||||
|
||||
Hi [$email],
|
||||
|
||||
Your email address has been verified.
|
||||
|
||||
Thank you,
|
||||
Siro Team";
|
||||
|
||||
mail($email, $subject, $bodyEmail, $headers);
|
||||
|
||||
jsonSuccess($message = "Your email address has been verified.");
|
||||
} else {
|
||||
jsonError($message ="Your email address could not be verified. Please try again.");
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user