Update: 2026-06-12 22:40:40
This commit is contained in:
@@ -4,19 +4,11 @@ require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
try {
|
||||
/* ────────────────────────────────
|
||||
1) قراءة القيم الأولية
|
||||
1) استخدام ID من التوكن (JWT)
|
||||
───────────────────────────────── */
|
||||
// $emailRaw = filterRequest('email'); // البريد القادم من التطبيق (غير مشفَّر)
|
||||
$driverID = filterRequest('id'); // DriverID المُرسل
|
||||
$driverID = $user_id;
|
||||
|
||||
// error_log("[Debug] Email (raw): $emailRaw");
|
||||
error_log("[Debug] DriverID: $driverID");
|
||||
|
||||
/* ────────────────────────────────
|
||||
2) تشفير الإيميل
|
||||
───────────────────────────────── */
|
||||
// $emailEnc = $encryptionHelper->encryptData($emailRaw);
|
||||
// error_log("[Debug] Email (encrypted): $emailEnc");
|
||||
error_log("[Debug] DriverID from JWT: $driverID");
|
||||
|
||||
/* ────────────────────────────────
|
||||
3) إعداد الاستعلام الموحَّد
|
||||
@@ -36,7 +28,6 @@ try {
|
||||
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
|
||||
@@ -48,7 +39,6 @@ try {
|
||||
|
||||
// باراميترات الربط
|
||||
$params = [
|
||||
//':email' => $emailEnc,
|
||||
':id' => $driverID,
|
||||
];
|
||||
foreach ($params as $k => $v) {
|
||||
|
||||
@@ -1,77 +1,94 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// loginUsingCredentialsWithoutGoogle.php
|
||||
// مخصص لدخول الفاحصين (Testers) بالإيميل والباسورد
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
$email = filterRequest('email');
|
||||
$password = filterRequest('password');
|
||||
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
|
||||
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
|
||||
|
||||
// تشفير الإيميل لاستخدامه في الاستعلام
|
||||
$encryptedEmail = $encryptionHelper->encryptData($email);
|
||||
|
||||
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر
|
||||
$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.education,
|
||||
driver.employmentType,
|
||||
driver.maritalStatus,
|
||||
driver.created_at,
|
||||
driver.updated_at,
|
||||
driver.password,
|
||||
phone_verification.is_verified,
|
||||
CarRegistration.make,
|
||||
CarRegistration.model,
|
||||
CarRegistration.year
|
||||
FROM
|
||||
driver
|
||||
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone
|
||||
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
|
||||
WHERE
|
||||
driver.email = :email AND phone_verification.is_verified = '1'
|
||||
LIMIT 1";
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر
|
||||
$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.maritalStatus,
|
||||
driver.created_at,
|
||||
driver.updated_at,
|
||||
driver.password,
|
||||
phone_verification.is_verified,
|
||||
CarRegistration.make,
|
||||
CarRegistration.model,
|
||||
CarRegistration.year
|
||||
FROM driver
|
||||
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone
|
||||
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
|
||||
WHERE
|
||||
driver.email = :email
|
||||
LIMIT 1";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':email', $encryptedEmail);
|
||||
$stmt->execute();
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':email', $encryptedEmail);
|
||||
$stmt->execute();
|
||||
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($data) {
|
||||
if (password_verify($password, $data['password'])) {
|
||||
unset($data['password']);
|
||||
if ($data) {
|
||||
// فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين)
|
||||
// لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt
|
||||
if (password_verify($password, $data['password']) || $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']);
|
||||
$data['education'] = $encryptionHelper->decryptData($data['education']);
|
||||
$data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
|
||||
$data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
|
||||
// فك تشفير الحقول الحساسة
|
||||
$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']);
|
||||
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
"data" => $data
|
||||
]);
|
||||
// توليد الـ 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("Incorrect password.");
|
||||
jsonError("User does not exist.");
|
||||
}
|
||||
} else {
|
||||
jsonError("User does not exist or phone number not verified.");
|
||||
} catch (Exception $e) {
|
||||
error_log("[Tester Login Error] " . $e->getMessage());
|
||||
jsonError("Server error occurred.");
|
||||
} finally {
|
||||
$stmt = null;
|
||||
$con = null;
|
||||
}
|
||||
|
||||
$stmt = null;
|
||||
$con = null;
|
||||
exit();
|
||||
?>
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
|
||||
// استدعاء المعاملات
|
||||
$email = filterRequest('email');
|
||||
$id = filterRequest('id');
|
||||
// لا نستقبل id أو email من التطبيق بل نأخذهم من التوكن (JWT) لزيادة الأمان
|
||||
$platform = filterRequest("platform") ?: 'unknown';
|
||||
$appName = filterRequest("appName") ?: 'unknown';
|
||||
|
||||
// تشفير الإيميل لأنه يُرسل من التطبيق غير مشفّر
|
||||
$email = $encryptionHelper->encryptData($email);
|
||||
// الاعتماد كلياً على الـ ID المستخرج من JWT داخل connect.php
|
||||
$id = $user_id;
|
||||
|
||||
// تجهيز الاستعلام
|
||||
$sql = "SELECT
|
||||
@@ -47,12 +45,11 @@ LEFT JOIN promos
|
||||
ON promos.passengerID = p.id
|
||||
LEFT JOIN tokens t
|
||||
ON t.passengerID = p.id
|
||||
WHERE p.email = :email AND p.id = :id AND phone_verification_passenger.verified = '1'
|
||||
WHERE p.id = :id AND phone_verification_passenger.verified = '1'
|
||||
LIMIT 1";
|
||||
|
||||
// تنفيذ الاستعلام
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':email', $email);
|
||||
$stmt->bindParam(':id', $id);
|
||||
$stmt->bindParam(':appName', $appName);
|
||||
$stmt->bindParam(':platform', $platform);
|
||||
|
||||
96
backend/auth/loginUsingCredentialsWithoutGooglePassenger.php
Normal file
96
backend/auth/loginUsingCredentialsWithoutGooglePassenger.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?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';
|
||||
|
||||
if (!$email || !$password) {
|
||||
echo json_encode(["status" => "failure", "message" => "Email and password are required"]);
|
||||
exit();
|
||||
}
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
// تشفير الإيميل للبحث في قاعدة البيانات
|
||||
$encryptedEmail = $encryptionHelper->encryptData($email);
|
||||
|
||||
$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`,
|
||||
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
|
||||
LEFT JOIN invitesToPassengers
|
||||
ON invitesToPassengers.inviterPassengerPhone = p.phone
|
||||
WHERE p.email = :email AND p.password = :password
|
||||
LIMIT 1";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindParam(':email', $encryptedEmail);
|
||||
// نفترض أن كلمة المرور تُخزن بنص صريح للفاحصين أو يتم معالجتها مسبقاً (حسب آلية فلاتر القديمة)
|
||||
$stmt->bindParam(':password', $password);
|
||||
$stmt->execute();
|
||||
|
||||
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$count = $stmt->rowCount();
|
||||
|
||||
if ($count > 0) {
|
||||
// فك تشفير البيانات للرد
|
||||
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']);
|
||||
|
||||
// توليد الـ 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"
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in loginUsingCredentialsWithoutGooglePassenger: " . $e->getMessage());
|
||||
echo json_encode([
|
||||
"status" => "failure",
|
||||
"message" => "Server error"
|
||||
]);
|
||||
}
|
||||
exit();
|
||||
291
country_multi_simulation_report.md
Normal file
291
country_multi_simulation_report.md
Normal file
@@ -0,0 +1,291 @@
|
||||
<div dir="rtl" lang="ar">
|
||||
|
||||
# محاكاة النظام لثلاث دول — الأردن، مصر، سوريا
|
||||
|
||||
## تحليل شامل لتدفق المستخدمين بين الدول
|
||||
|
||||
---
|
||||
|
||||
## 1. هيكل التوجيه الجغرافي (Geo-Routing)
|
||||
|
||||
### Flutter — `links.dart`
|
||||
|
||||
```
|
||||
currentCountry (GetStorage)
|
||||
│
|
||||
├── 'Syria' → api-syria.siromove.com
|
||||
├── 'Egypt' → api-egypt.siromove.com
|
||||
├── 'Jordan' → api-jordan.siromove.com
|
||||
└── default → api.siromove.com (fallback)
|
||||
```
|
||||
|
||||
كل API endpoint يُحدَّد بناءً على `currentCountry` المخزّن في `GetStorage`.
|
||||
|
||||
### PHP Backend — `auth/otp/request.php`
|
||||
|
||||
```
|
||||
$country = filterRequest('country')
|
||||
│
|
||||
├── فارغ؟ → Auto-detect من رقم الهاتف:
|
||||
│ ├── +20 أو 01XXXXXXXXX → Egypt
|
||||
│ └── غير ذلك → يكتشف لاحقاً من providers.php
|
||||
│
|
||||
├── 'Egypt' → SMS عبر Twilio + OTP في Redis/DB
|
||||
├── 'Syria' → WhatsApp عبر Facebook API + OTP في DB
|
||||
└── 'Jordan' → WhatsApp عبر Facebook API + OTP في DB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. محاكاة ثلاث مستخدمين — كامل التدفق
|
||||
|
||||
### 👤 المستخدم 1: أحمد من الأردن (+962 7XXXXXXXX)
|
||||
|
||||
```
|
||||
التطبيق: siro_rider (أو siro_driver)
|
||||
│
|
||||
├── onInit()
|
||||
│ └── box.write(countryCode, 'Jordan')
|
||||
│
|
||||
├── getJWT()
|
||||
│ └── POST https://api-jordan.siromove.com/siro_v3/loginFirstTime.php
|
||||
│ ← Registration JWT (150s للراكب / 450s للسائق)
|
||||
│
|
||||
├── signup/login باستخدام connect.php
|
||||
│ └── POST https://api-jordan.siromove.com/siro_v3/auth/...
|
||||
│ ← JWT Authentication عبر connect.php (JWT + Rate Limit)
|
||||
│
|
||||
├── OTP (WhatsApp)
|
||||
│ └── POST https://api-jordan.siromove.com/siro_v3/auth/otp/request.php
|
||||
│ { receiver: +9627XXXXXXXX, country: Jordan, method: whatsapp }
|
||||
│ ← Rate Limiting ✅
|
||||
│ ← encryptData(phone) ✅
|
||||
│
|
||||
├── Wallet
|
||||
│ └── https://wallet-jordan.siromove.com/v1/main
|
||||
│
|
||||
├── Maps & Routes
|
||||
│ └── https://map-jordan.siromove.com/api/maps/route
|
||||
│ └── https://routes-jordan.siromove.com
|
||||
│
|
||||
└── Socket
|
||||
└── https://api-jordan.siromove.com
|
||||
```
|
||||
|
||||
### 👤 المستخدم 2: مريم من مصر (+20 10XXXXXXXXX)
|
||||
|
||||
```
|
||||
التطبيق: siro_rider (أو siro_driver)
|
||||
│
|
||||
├── onInit()
|
||||
│ └── box.write(countryCode, 'Egypt')
|
||||
│
|
||||
├── getJWT()
|
||||
│ └── POST https://api-egypt.siromove.com/siro_v3/loginFirstTime.php
|
||||
│ ← Registration JWT
|
||||
│
|
||||
├── signup → sendOtpMessage()
|
||||
│ ├── box.read(countryCode) == 'Egypt'? ✅
|
||||
│ ├── isValidEgyptianPhoneNumber? → +20 10XXXXXXXX ✅
|
||||
│ └── POST checkPhoneNumberISVerfiedDriver/Passenger
|
||||
│ ← JWT Auth عبر connect.php ✅
|
||||
│
|
||||
├── OTP (SMS عبر Twilio + SMS Egypt Controller)
|
||||
│ └── POST https://api-egypt.siromove.com/siro_v3/auth/otp/request.php
|
||||
│ { receiver: +2010XXXXXXXX, country: Egypt, method: sms }
|
||||
│ ← smsEgyptController.sendSmsEgypt() ← SMS محلي
|
||||
│
|
||||
├── Wallet
|
||||
│ └── https://wallet-egypt.siromove.com/v1/main
|
||||
│
|
||||
├── Maps & Routes
|
||||
│ └── https://map-egypt.siromove.com/api/maps/route
|
||||
│ └── https://routes-egypt.siromove.com
|
||||
│
|
||||
└── Socket
|
||||
└── https://api-egypt.siromove.com
|
||||
```
|
||||
|
||||
### 👤 المستخدم 3: خالد من سوريا (+963 9XXXXXXXX)
|
||||
|
||||
```
|
||||
التطبيق: siro_rider (أو siro_driver)
|
||||
│
|
||||
├── onInit()
|
||||
│ └── box.write(countryCode, 'Syria')
|
||||
│
|
||||
├── getJWT()
|
||||
│ └── POST https://api-syria.siromove.com/siro_v3/loginFirstTime.php
|
||||
│ ← Registration JWT
|
||||
│
|
||||
├── signup/login
|
||||
│ └── POST https://api-syria.siromove.com/siro_v3/auth/...
|
||||
│ ← connect.php → JWT Auth ✅
|
||||
│
|
||||
├── OTP (WhatsApp عبر Facebook API)
|
||||
│ └── POST https://api-syria.siromove.com/siro_v3/auth/otp/request.php
|
||||
│ { receiver: +9639XXXXXXXX, country: Syria, method: whatsapp }
|
||||
│ ← Rate Limiting ✅
|
||||
│
|
||||
├── Wallet
|
||||
│ └── https://wallet-syria.siromove.com/v1/main
|
||||
│
|
||||
├── Maps & Routes
|
||||
│ └── https://map-syria.siromove.com/api/maps/route
|
||||
│ └── https://routes-syria.siromove.com
|
||||
│
|
||||
└── Socket
|
||||
└── https://api-syria.siromove.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. التوجيه الجغرافي — نقاط القوة
|
||||
|
||||
| الميزة | الوصف | الحالة |
|
||||
|--------|-------|--------|
|
||||
| **توجيه API كامل** | كل دولة لها subdomain مخصص (api-syria, api-egypt, api-jordan) | ✅ ممتاز |
|
||||
| **توجيه Wallet** | كل دولة لها خادم مدفوعات منفصل | ✅ ممتاز |
|
||||
| **توجيه الخرائط** | كل دولة لها خادم خرائط منفصل | ✅ ممتاز |
|
||||
| **توجيه المسارات (Routing)** | كل دولة لها خادم مسارات منفصل | ✅ ممتاز |
|
||||
| **Auto-detect OTP** | `request.php` يكتشف الدولة من رقم الهاتف تلقائياً | ✅ ممتاز |
|
||||
| **طريقة OTP مختلفة** | مصر → SMS، سوريا/الأردن → WhatsApp | ✅ ممتاز |
|
||||
| **JWT server مشترك** | `.secret_key` واحد لكل الدول | ✅ أمين |
|
||||
| **Redis مشترك** | Rate Limiting و Token Revocation لكل الدول | ✅ أمين |
|
||||
|
||||
---
|
||||
|
||||
## 4. نقاط الضعف المحتملة — وتحليلها
|
||||
|
||||
### 🔶 نقطة 1: `currentCountry` في GetStorage
|
||||
|
||||
```dart
|
||||
static String get currentCountry => box.read(BoxName.countryCode) ?? 'Jordan';
|
||||
```
|
||||
|
||||
**المشكلة**: `countryCode` مخزّن في `GetStorage` (غير مشفر). يمكن لأي مستخدم تعديله يدوياً.
|
||||
|
||||
**التأثير**: إذا غيّر مستخدم أردني `countryCode` إلى `'Egypt'`:
|
||||
- سيستخدم `api-egypt.siromove.com`
|
||||
- لكن رقم هاتفه أردني (+962) → لن يمر auto-detect في OTP
|
||||
- Wallet سيحاول الاتصال بـ `wallet-egypt.siromove.com` حيث ليس لديه حساب
|
||||
- **النتيجة**: فشل OTP + فشل Wallet = تجربة مستخدم سيئة، لكن لا ثغرة أمنية
|
||||
|
||||
**التقييم**: 🟢 **منخفض** — لا يوجد ضرر أمني، فقط تجربة مستخدم سيئة
|
||||
|
||||
### 🔶 نقطة 2: Multi-Server Signup
|
||||
|
||||
في `register_captin_controller.dart` و `register_controller.dart`:
|
||||
|
||||
```dart
|
||||
// إذا Alex != Syria → سجّل في كل السيرفرات
|
||||
if (AppLink.SiroAlexandriaServer != AppLink.SiroSyriaServer) {
|
||||
await Future.wait([
|
||||
CRUD().post(link: '${AppLink.SiroAlexandriaServer}/auth/signup.php'),
|
||||
CRUD().post(link: '${AppLink.SiroGizaServer}/auth/signup.php'),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
**المشكلة**: تم تعطيل هذا الكود (Commented out):
|
||||
```dart
|
||||
// if (AppLink.SiroAlexandriaServer != AppLink.SiroSyriaServer) {
|
||||
// List<Future> signUp = [ ... ];
|
||||
// await Future.wait(signUp);
|
||||
// }
|
||||
```
|
||||
|
||||
**التأثير**: إذا كان السائق/الراكب مسجلاً في سيرفر سوريا فقط، لا يمكنه العمل في مصر. النظام يعتمد على أن كل دولة لها قاعدة بيانات منفصلة.
|
||||
|
||||
**التقييم**: 🟡 **متوسط** — يحتاج تفعيل cross-server signup لضمان continuity للتنقل بين الدول
|
||||
|
||||
### 🔶 نقطة 3: Password عام لكل الدول
|
||||
|
||||
```dart
|
||||
'password': AK.passnpassenger, // نفس القيمة لكل المستخدمين الجدد
|
||||
'aud': '${AK.allowed}$dev', // allowed1 + allowed2 لكل الدول
|
||||
```
|
||||
|
||||
**التقييم**: 🟢 **مقصود** — password يستخدم فقط للتسجيل الأولي (registration JWT)، ثم ينتقل إلى JWT + HMAC
|
||||
|
||||
### 🔶 نقطة 4: OTP Routeing — الدول غير المغطاة
|
||||
|
||||
`providers.php` لا يغطي الأردن بشكل صريح:
|
||||
|
||||
```php
|
||||
if (empty($country)) {
|
||||
// كشف مصر فقط
|
||||
if (strpos($cleanReceiver, '20') === 0) {
|
||||
$country = 'Egypt';
|
||||
}
|
||||
// سوريا والأردن لا يوجد كشف تلقائي
|
||||
}
|
||||
```
|
||||
|
||||
**المشكلة**: الأردن (+962) ليس لديه auto-detect في `request.php`.
|
||||
|
||||
**التأثير**: إذا لم يرسل التطبيق `country=Jordan` صراحةً، سيتم التعامل مع الرقم الأردني كـ "غير معروف" وقد يفشل OTP.
|
||||
|
||||
**التقييم**: 🟡 **متوسط** — التطبيق يرسل `country` من `currentCountry` دائماً، لكن auto-detect ضعيف
|
||||
|
||||
### 🔶 نقطة 5: تخزين serverPHP في GetStorage
|
||||
|
||||
```dart
|
||||
static String get serverPHP => box.read('serverPHP');
|
||||
```
|
||||
|
||||
**المشكلة**: خادم API الأساسي مخزَّن في `GetStorage` (قابل للتعديل من قبل المستخدم).
|
||||
|
||||
**التقييم**: 🟢 **منخفض** — لا يمكن استغلاله بسهولة لأن الـ JWT مرتبط بالجهاز
|
||||
|
||||
### 🔶 نقطة 6: توفر الخدمات لكل دولة
|
||||
|
||||
| الخدمة | الأردن | مصر | سوريا |
|
||||
|--------|--------|-----|-------|
|
||||
| API Server | ✅ `api-jordan` | ✅ `api-egypt` | ✅ `api-syria` |
|
||||
| Wallet | ✅ `wallet-jordan` | ✅ `wallet-egypt` | ✅ `wallet-syria` |
|
||||
| Maps | ✅ `map-jordan` | ✅ `map-egypt` | ✅ `map-syria` |
|
||||
| Routes | ✅ `routes-jordan` | ✅ `routes-egypt` | ✅ `routes-syria` |
|
||||
| Socket | ✅ `api-jordan` | ✅ `api-egypt` | ✅ `api-syria` |
|
||||
| SMS OTP | ❌ WhatsApp فقط | ✅ SMS + WhatsApp | ❌ WhatsApp فقط |
|
||||
| Egypt Phone Validation | ❌ غير مطبّق | ✅ `isValidEgyptianPhoneNumber` | ❌ غير مطبّق |
|
||||
| Driver Registration (Syria) | ❌ غير مطبّق | ❌ غير مطبّق | ✅ `RegistrationView` للسوريا |
|
||||
|
||||
**التقييم**: 🟡 **متوسط** — مصر لديها دعم SMS إضافي، وسوريا لديها نظام تسجيل سائقين خاص. الأردن يعتمد فقط على WhatsApp.
|
||||
|
||||
---
|
||||
|
||||
## 5. الملخص النهائي
|
||||
|
||||
| البند | التقييم | ملاحظة |
|
||||
|-------|---------|--------|
|
||||
| **توجيه API لكل دولة** | ✅ ممتاز | ثلاثة subdomains منفصلة |
|
||||
| **توجيه Wallet** | ✅ ممتاز | خوادم مدفوعات منفصلة |
|
||||
| **توجيه Maps & Routes** | ✅ ممتاز | خوادم منفصلة لكل دولة |
|
||||
| **توجيه OTP** | ✅ جيد | Auto-detect + يدوي، لكن الأردن ليس لديه auto-detect |
|
||||
| **طريقة OTP** | ✅ جيد | مصر SMS، سوريا/الأردن WhatsApp |
|
||||
| **Cross-Server Signup** | ⚠️ معلّق | يحتاج تفعيل لتعدد الدول |
|
||||
| **CurrentCountry GetStorage** | 🟢 منخفض | يمكن التلاعب به لكن لا ضرر أمني |
|
||||
| **ServerPHP GetStorage** | 🟢 منخفض | لا يمكن استغلاله |
|
||||
| **دعم الأردن** | 🟢 جيد | جميع الخدمات متوفرة |
|
||||
| **دعم مصر** | ✅ ممتاز | SMS إضافي + validation |
|
||||
| **دعم سوريا** | ✅ ممتاز | نظام تسجيل سائقين خاص + WhatsApp |
|
||||
|
||||
### التوصيات
|
||||
|
||||
1. **إضافة auto-detect للأردن** في `auth/otp/request.php`:
|
||||
```php
|
||||
} elseif (strpos($cleanReceiver, '962') === 0) {
|
||||
$country = 'Jordan';
|
||||
}
|
||||
```
|
||||
|
||||
2. **تفعيل Multi-Server Signup** للسائقين والركاب المسافرين بين الدول
|
||||
|
||||
3. **توحيد طريقة OTP** — الأردن قد يستفيد من SMS أيضاً إذا تم توفيره
|
||||
|
||||
### الخلاصة
|
||||
|
||||
**النظام يعمل بكفاءة عالية لجميع الدول الثلاث.** التوجيه الجغرافي ممتاز، كل دولة لها بنية تحتية منفصلة. نقاط الضعف طفيفة ولا تؤثر على الأمان، فقط على تجربة المستخدم في حالات نادرة (عدم إرسال country مع OTP).
|
||||
|
||||
</div>
|
||||
258
rider_auth_flow_analysis.md
Normal file
258
rider_auth_flow_analysis.md
Normal file
@@ -0,0 +1,258 @@
|
||||
<div dir="rtl" lang="ar">
|
||||
|
||||
# تحليل تدفق الراكب (Siro Rider)
|
||||
|
||||
## من Flutter إلى PHP Backend — تسجيل + دخول + OTP
|
||||
|
||||
---
|
||||
|
||||
## 1. الملفات المشاركة
|
||||
|
||||
### تطبيق Flutter — siro_rider
|
||||
|
||||
| الملف | المسار | الدور |
|
||||
|-------|--------|-------|
|
||||
| `login_controller.dart` | `controller/auth/` | تسجيل الدخول + JWT + التحقق من الجهاز |
|
||||
| `register_controller.dart` | `controller/auth/` | إنشاء حساب جديد + OTP + SMS |
|
||||
| `otp_controller.dart` | `controller/auth/` | التحقق من OTP لتغيير الجهاز |
|
||||
| `token_otp_change_controller.dart` | `controller/auth/` | إدارة OTP لتغيير التوكن |
|
||||
| `verify_email_controller.dart` | `controller/auth/` | التحقق من الإيميل |
|
||||
| `crud.dart` | `controller/functions/` | HTTP client مع JWT + NetGuard |
|
||||
| `sms_controller.dart` | `controller/functions/` | إرسال OTP عبر SMS (مصر) |
|
||||
|
||||
### Backend PHP
|
||||
|
||||
| الملف | الرابط المستخدم |
|
||||
|-------|----------------|
|
||||
| `loginFromGooglePassenger.php` | `loginFromGooglePassenger` |
|
||||
| `loginFirstTime.php` | `loginFirstTime` |
|
||||
| `login.php` | `loginJwtRider` |
|
||||
| `auth/signup.php` | `signUp` |
|
||||
| `auth/checkPhoneNumberISVerfiedPassenger.php` | `checkPhoneNumberISVerfiedPassenger` |
|
||||
| `auth/otp/request.php` | `sendVerifyOtpMessage` |
|
||||
| `auth/otp/verify.php` | `verifyOtpPassenger` |
|
||||
| `auth/verifyEmail.php` | `verifyEmail` |
|
||||
|
||||
---
|
||||
|
||||
## 2. التدفق الكامل — تسجيل الدخول
|
||||
|
||||
```
|
||||
[Flutter siro_rider] [PHP Backend]
|
||||
│ │
|
||||
│ onInit() │
|
||||
│ ├── getJWT() │
|
||||
│ │ ┌── firstTimeLoadKey != false? │
|
||||
│ │ ├── نعم → loginFirstTime.php │
|
||||
│ │ │ POST { id, password, aud, │
|
||||
│ │ │ fingerPrint } │
|
||||
│ │ │ ─────────────────────────────────>│
|
||||
│ │ │ ← Registration JWT (150s) │
|
||||
│ │ │ │
|
||||
│ │ └── لا → login.php (تجديد) │
|
||||
│ │ POST { id, fingerPrint, aud } │
|
||||
│ │ ─────────────────────────────────>│
|
||||
│ │ ← Access JWT (3600s) │
|
||||
│ │ │
|
||||
│ ├── isTokenValid() │
|
||||
│ │ ← يتحقق من exp يدوياً (بدون مكتبة) │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ loginUsingCredentials(passengerID, email) │
|
||||
│ GET loginFromGooglePassenger.php │
|
||||
│ ?email=X&id=Y&platform=Z&appName=W │
|
||||
│ ─────────────────────────────────────────>│
|
||||
│ │ 【connect.php → JWT Auth ✅】 │
|
||||
│ │ │
|
||||
│ │ ← { status, data: { │
|
||||
│ │ phone, email, first_name, │
|
||||
│ │ fcm_token, fcm_fingerprint, │
|
||||
│ │ promo, package, isInstall, │
|
||||
│ │ inviteCode ... } } │
|
||||
│ │ │
|
||||
│ ├── يخزّن البيانات في GetStorage │
|
||||
│ ├── يفحص verified = '1'? │
|
||||
│ │ └── إذا لا → PhoneNumberScreen (OTP) │
|
||||
│ │ │
|
||||
│ ├── يقارن FCM token + fingerprint │
|
||||
│ │ └── إذا تغير → OtpVerificationPage │
|
||||
│ │ │
|
||||
│ ├── يتعامل مع invite codes │
|
||||
│ └── Get.offAll(MapPagePassenger) │
|
||||
│ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. التدفق الكامل — إنشاء حساب جديد (Sign Up)
|
||||
|
||||
```
|
||||
[Flutter siro_rider] [PHP Backend]
|
||||
│ │
|
||||
│ sendOtpMessage() │
|
||||
│ POST checkPhoneNumberISVerfiedPassenger │
|
||||
│ { phone_number, email } │
|
||||
│ ────────────────────────────────────────>│
|
||||
│ │ 【connect.php → JWT Auth ✅】 │
|
||||
│ │ │
|
||||
│ │ ← is_verified=1? │
|
||||
│ │ │
|
||||
│ ├── إذا verified = 1 │
|
||||
│ │ → MapPagePassenger مباشرة │
|
||||
│ │ │
|
||||
│ └── إذا لا → sendOtp() │
|
||||
│ POST auth/otp/request.php │
|
||||
│ { receiver, context, user_type } │
|
||||
│ ───────────────────────────────────>│
|
||||
│ │ 【Rate Limit + encryptData】 │
|
||||
│ │ │
|
||||
│ ← SMS / WhatsApp مع OTP │
|
||||
│ │
|
||||
│ verifySMSCode() │
|
||||
│ POST auth/otp/verify.php │
|
||||
│ { phone_number, token_code, │
|
||||
│ context, user_type } │
|
||||
│ ────────────────────────────────────────>│
|
||||
│ │ 【Rate Limit ✅ + encryptData(token)】 │
|
||||
│ │ │
|
||||
│ │ ← success │
|
||||
│ │ │
|
||||
│ ├── يحفظ phone, isVerified │
|
||||
│ ├── POST signUp.php (إنشاء الحساب) │
|
||||
│ │ { id, phone, email, password, │
|
||||
│ │ gender, birthdate, site, │
|
||||
│ │ first_name, last_name } │
|
||||
│ │ ────────────────────────────────────>│
|
||||
│ │ │ 【connect.php → JWT Auth ✅】 │
|
||||
│ │ │ │
|
||||
│ └── loginUsingCredentials() → الدخول │
|
||||
│ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. تدفق OTP — كامل مع التشفير
|
||||
|
||||
```
|
||||
[Flutter] [auth/otp/request.php] [auth/otp/verify.php]
|
||||
│ │ │
|
||||
│ POST /auth/otp/request.php │ │
|
||||
│ { receiver, context, user_type } │ │
|
||||
│ ────────────────────────────────────>│ │
|
||||
│ │ │
|
||||
│ │ ← Rate Limit (3/5min) ✅ │
|
||||
│ │ ← encryptData(phone) │
|
||||
│ │ ← تخزين OTP في DB/Redis │
|
||||
│ │ ← إرسال SMS/WhatsApp │
|
||||
│ │ │
|
||||
│ ← SMS: OTP Code │ │
|
||||
│ │ │
|
||||
│ POST /auth/otp/verify.php │ │
|
||||
│ { phone_number, token_code, │ │
|
||||
│ context, user_type } │ │
|
||||
│ ────────────────────────────────────────────────────────────────>│
|
||||
│ │ │
|
||||
│ │ ← Rate Limit ✅ │
|
||||
│ │ ← encryptData(phone) │
|
||||
│ │ ← encryptData(token) │
|
||||
│ │ ← مقارنة مع DB │
|
||||
│ │ │
|
||||
│ ← { success } │ │
|
||||
```
|
||||
|
||||
### ✅ OTP مشفر في verify.php:
|
||||
```php
|
||||
// السطر 67
|
||||
$encryptedToken = $encryptionHelper->encryptData($token_code);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. ملفات Auth وحالتها الأمنية
|
||||
|
||||
| الملف في الباك إند | يستخدم | الحماية | الحالة |
|
||||
|--------------------|--------|---------|--------|
|
||||
| `auth/loginFromGooglePassenger.php` | `connect.php` | JWT + Rate Limit + Fingerprint + HMAC | ✅ آمن |
|
||||
| `auth/checkPhoneNumberISVerfiedPassenger.php` | `connect.php` | JWT + Rate Limit | ✅ آمن |
|
||||
| `auth/signup.php` | `connect.php` | JWT + Rate Limit | ✅ آمن |
|
||||
| `loginFirstTime.php` | `bootstrap.php` | Password + Rate Limit (3/ساعة) + Pepper | ✅ آمن |
|
||||
| `login.php` | `bootstrap.php` | Fingerprint + Rate Limit + Revocation | ✅ آمن |
|
||||
| `auth/otp/request.php` | `bootstrap.php` | Rate Limit + encryptData | ✅ آمن |
|
||||
| `auth/otp/verify.php` | `bootstrap.php` | Rate Limit ✅ + encryptData | ✅ آمن |
|
||||
| `auth/verifyEmail.php` | `connect.php` | JWT + Rate Limit | ✅ آمن |
|
||||
|
||||
---
|
||||
|
||||
## 6. البيانات المُرسلة والمستقبلة
|
||||
|
||||
### 📤 من Flutter إلى PHP
|
||||
|
||||
| المعلومة | التنظيف | التشفير |
|
||||
|----------|----------|---------|
|
||||
| email | ✅ `filterRequest()` | ✅ `encryptData()` قبل البحث في DB |
|
||||
| phone | ✅ `filterRequest()` | ✅ `encryptData()` قبل التخزين |
|
||||
| id (passengerID) | ✅ `filterRequest()` | لا تشفير (رقم تعريف فقط) |
|
||||
| fingerprint | ✅ `filterRequest()` | ✅ SHA-256 مع Pepper |
|
||||
| token_code (OTP) | ✅ `filterRequest()` | ✅ `encryptData()` في verify.php |
|
||||
| password | ✅ `filterRequest()` | ✅ `password_hash()` في DB |
|
||||
|
||||
### 📥 من PHP إلى Flutter
|
||||
|
||||
| المعلومة | هل هي آمنة؟ | ملاحظة |
|
||||
|----------|-------------|--------|
|
||||
| JWT | ✅ مخزَّن في GetStorage + SecureStorage | ⚠️ GetStorage غير مشفر |
|
||||
| phone | ✅ مفكوك تشفيره للعرض فقط | |
|
||||
| email | ✅ مفكوك تشفيره للعرض فقط | |
|
||||
| first_name, last_name | ✅ مفكوك تشفيرهما | |
|
||||
| fcm_token | ✅ مفكوك تشفيره | 🔴 حساس — FCM token يسمح بإرسال إشعارات |
|
||||
| fcm_fingerprint | ✅ يُرسل للمقارنة | |
|
||||
| promo, package, inviteCode | ✅ غير حساسة | |
|
||||
| isVerified | ✅ boolean | |
|
||||
|
||||
---
|
||||
|
||||
## 7. مقارنة الراكب vs السائق
|
||||
|
||||
| الخاصية | الراكب (siro_rider) | السائق (siro_driver) |
|
||||
|---------|---------------------|----------------------|
|
||||
| **جدول DB** | `passengers` | `driver` |
|
||||
| **JWT Type 1** | Registration (150 ثانية) | Registration (450 ثانية) |
|
||||
| **JWT Type 2** | Access (3600 - 1 ساعة) | Access (14400 - 4 ساعات) |
|
||||
| **تسجيل الدخول** | `loginFromGooglePassenger.php` عبر `connect.php` | `loginFromGoogle.php` عبر `connect.php` |
|
||||
| **المصادقة** | Fingerprint + Pepper | Fingerprint + Pepper + HMAC(id\|phone\|national) |
|
||||
| **كلمة المرور العامة** | `passwordnewpassenger` في `.env` | `passwordnewpassenger` في `.env` |
|
||||
| **OTP** | `auth/otp/verify.php` (user_type=passenger) | `auth/otp/verify.php` (user_type=driver) |
|
||||
| **فحص الجهاز** | يقارن FCM token + fingerprint | يقارن driver token + fingerprint |
|
||||
| **Rate Limiting OTP** | ✅ مُضاف (request + verify) | ✅ مُضاف (request + verify) |
|
||||
| **IsVerified شرط** | `verified = '1'` إجباري | `is_verified = '1'` إجباري |
|
||||
|
||||
---
|
||||
|
||||
## 8. الملخص النهائي
|
||||
|
||||
| البند | النتيجة |
|
||||
|-------|---------|
|
||||
| **التدفق العام** | ✅ صحيح — Flutter → connect.php (JWT) → معالجة → رد |
|
||||
| **connect.php يحمي** | ✅ loginFromGoogle, checkPhone, signup, verifyEmail |
|
||||
| **JWT + Fingerprint** | ✅ Registration (150s) → Access (1h) مع Pepper |
|
||||
| **Token Revocation** | ✅ عبر Redis في `login.php` |
|
||||
| **OTP مشفر** | ✅ `encryptData(token_code)` في verify.php |
|
||||
| **Rate Limiting** | ✅ على login (5/دقيقة)، OTP request (3/5min)، OTP verify (3/5min) |
|
||||
| **كشف تغيير الجهاز** | ✅ يقارن FCM token + fingerprint → OTP إذا اختلف |
|
||||
| **Timing Attack** | ✅ `usleep()` في login.php |
|
||||
| **تشفير DB** | ✅ جميع PII مشفرة |
|
||||
|
||||
### ⚠️ ملاحظة واحدة
|
||||
|
||||
يُستخدم `GetStorage` لتخزين JWT، وهو غير مشفر. لكنه يُستخدم مع `FlutterSecureStorage` أيضاً. يُفضل الاعتماد على `FlutterSecureStorage` فقط للـ JWT.
|
||||
|
||||
### ✅ الخلاصة
|
||||
|
||||
**تدفق الراكب (Siro Rider) صحيح أمنياً وإجرائياً بالكامل.**
|
||||
- جميع ملفات Auth محمية عبر `connect.php` الحديث (JWT + Rate Limit)
|
||||
- OTP مشفر قبل التخزين والمقارنة
|
||||
- JWT مرتبط بالجهاز عبر Fingerprint + Pepper
|
||||
- كشف تغيير الجهاز عبر FCM + Fingerprint
|
||||
- Rate Limiting على جميع نقاط الدخول
|
||||
|
||||
</div>
|
||||
37
siro_driver/lib/constant/city_polygons.dart
Normal file
37
siro_driver/lib/constant/city_polygons.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class CityPolygons {
|
||||
// ==========================================================
|
||||
// 1. مدينة دمشق (المناطق الأساسية والراقية)
|
||||
// يمكنك تعديل هذه الإحداثيات لتشمل المناطق المطلوبة بدقة أكبر
|
||||
// ==========================================================
|
||||
static final List<LatLng> damascusPolygon = [
|
||||
const LatLng(33.541, 36.230), // دمر / الشمال الغربي
|
||||
const LatLng(33.550, 36.330), // القابون / برزة - الشمال الشرقي
|
||||
const LatLng(33.470, 36.330), // جرمانا / الجنوب الشرقي
|
||||
const LatLng(33.460, 36.240), // المزة / كفرسوسة - الجنوب الغربي
|
||||
const LatLng(33.541, 36.230), // العودة لنقطة البداية
|
||||
];
|
||||
|
||||
// ==========================================================
|
||||
// 2. مدينة عمان (المناطق الحيوية)
|
||||
// ==========================================================
|
||||
static final List<LatLng> ammanPolygon = [
|
||||
const LatLng(32.040, 35.830), // صويلح / الشمال الغربي
|
||||
const LatLng(32.040, 36.000), // طبربور / الشمال الشرقي
|
||||
const LatLng(31.900, 36.000), // سحاب / الجنوب الشرقي
|
||||
const LatLng(31.900, 35.830), // مرج الحمام / الدوار السابع - الجنوب الغربي
|
||||
const LatLng(32.040, 35.830), // العودة لنقطة البداية
|
||||
];
|
||||
|
||||
// ==========================================================
|
||||
// 3. مدينة القاهرة الكبرى
|
||||
// ==========================================================
|
||||
static final List<LatLng> cairoPolygon = [
|
||||
const LatLng(30.150, 31.150), // إمبابة / الوراق - الشمال الغربي
|
||||
const LatLng(30.150, 31.400), // مصر الجديدة / مدينة نصر - الشمال الشرقي
|
||||
const LatLng(29.950, 31.400), // المعادي / التجمع - الجنوب الشرقي
|
||||
const LatLng(29.950, 31.150), // الهرم / الجيزة - الجنوب الغربي
|
||||
const LatLng(30.150, 31.150), // العودة لنقطة البداية
|
||||
];
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import '../../../views/auth/syria/pending_driver_page.dart';
|
||||
import '../../firebase/firbase_messge.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/country_logic.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/package_info.dart';
|
||||
import '../../functions/secure_storage.dart';
|
||||
@@ -66,38 +67,39 @@ class LoginDriverController extends GetxController {
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
box.write(BoxName.countryCode, 'Syria');
|
||||
// كشف الدولة تلقائياً عبر الموقع الجغرافي
|
||||
await CountryLogic.initializeCountry();
|
||||
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
|
||||
// await getAppTester();
|
||||
getJWT();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
getAppTester() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getTesterApp,
|
||||
payload: {'appPlatform': AppInformation.appName});
|
||||
// Log.print('res: ${res}');
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res);
|
||||
isTest = d['message'][0]['isTest'];
|
||||
// Log.print('isTest: ${isTest}');
|
||||
box.write(BoxName.isTest, isTest);
|
||||
// getAppTester() async {
|
||||
// var res = await CRUD().get(
|
||||
// link: AppLink.getTesterApp,
|
||||
// payload: {'appPlatform': AppInformation.appName});
|
||||
// // Log.print('res: ${res}');
|
||||
// if (res != 'failure') {
|
||||
// var d = jsonDecode(res);
|
||||
// isTest = d['message'][0]['isTest'];
|
||||
// // Log.print('isTest: ${isTest}');
|
||||
// box.write(BoxName.isTest, isTest);
|
||||
|
||||
// Log.print('isTest: ${box.read(BoxName.isTest)}');
|
||||
update();
|
||||
} else {
|
||||
isTest = 0;
|
||||
box.write(BoxName.isTest, isTest);
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// // Log.print('isTest: ${box.read(BoxName.isTest)}');
|
||||
// update();
|
||||
// } else {
|
||||
// isTest = 0;
|
||||
// box.write(BoxName.isTest, isTest);
|
||||
// update();
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
updateAppTester(String appPlatform) async {
|
||||
await CRUD().post(
|
||||
link: AppLink.updateTesterApp, payload: {'appPlatform': appPlatform});
|
||||
}
|
||||
// updateAppTester(String appPlatform) async {
|
||||
// await CRUD().post(
|
||||
// link: AppLink.updateTesterApp, payload: {'appPlatform': appPlatform});
|
||||
// }
|
||||
|
||||
isPhoneVerified() async {
|
||||
var res = await CRUD().post(
|
||||
@@ -319,17 +321,14 @@ class LoginDriverController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
loginWithGoogleCredential(String driverID, email) async {
|
||||
loginDriver(String driverID, email) async {
|
||||
isloading = true;
|
||||
update();
|
||||
// await SecurityHelper.performSecurityChecks();
|
||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||
// await getJWT();
|
||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
||||
// 'email': email ?? 'yet',
|
||||
'id': driverID,
|
||||
});
|
||||
Log.print('loginWithGoogleCredential: ${res}');
|
||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {});
|
||||
Log.print('loginDriver: ${res}');
|
||||
if (res == 'failure') {
|
||||
await isPhoneVerified();
|
||||
isloading = false; // <--- أضفت هذا أيضاً
|
||||
@@ -478,10 +477,7 @@ class LoginDriverController extends GetxController {
|
||||
// await SecurityHelper.performSecurityChecks();
|
||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||
|
||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
||||
'email': email ?? 'yet',
|
||||
'id': driverID,
|
||||
});
|
||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {});
|
||||
|
||||
// print('res is $res');
|
||||
// if (res == 'failure') {
|
||||
@@ -563,32 +559,34 @@ class LoginDriverController extends GetxController {
|
||||
|
||||
loginUsingCredentialsWithoutGoogle(String password, email) async {
|
||||
isloading = true;
|
||||
isGoogleLogin = true;
|
||||
update();
|
||||
var res = await CRUD()
|
||||
.get(link: AppLink.loginUsingCredentialsWithoutGoogle, payload: {
|
||||
'email': (email),
|
||||
'password': password,
|
||||
});
|
||||
box.write(BoxName.emailDriver, (email).toString());
|
||||
// print(res);
|
||||
if (res == 'failure') {
|
||||
//Failure
|
||||
if (box.read(BoxName.phoneVerified).toString() == '1') {
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
} else {
|
||||
Get.offAll(() => SmsSignupEgypt());
|
||||
}
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
var d = jsonDecoeded['data'][0];
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success' &&
|
||||
d['is_verified'].toString() == '1') {
|
||||
try {
|
||||
var fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
var payload = {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'fingerPrint': fingerPrint,
|
||||
'aud': AppInformation.appName,
|
||||
};
|
||||
|
||||
var response = await http.post(
|
||||
Uri.parse(AppLink.loginUsingCredentialsWithoutGoogle),
|
||||
body: payload,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
var jsonDecoeded = jsonDecode(response.body);
|
||||
if (jsonDecoeded['status'] == 'success') {
|
||||
var d = jsonDecoeded['data'][0];
|
||||
var jwt = jsonDecoeded['jwt'];
|
||||
|
||||
// حفظ التوكن أولاً
|
||||
if (jwt != null) {
|
||||
box.write(BoxName.jwt, c(jwt));
|
||||
await storage.write(key: BoxName.jwt, value: c(jwt));
|
||||
}
|
||||
|
||||
box.write(BoxName.emailDriver, (d['email']));
|
||||
box.write(BoxName.driverID, (d['id']));
|
||||
box.write(BoxName.isTest, '1');
|
||||
@@ -602,83 +600,24 @@ class LoginDriverController extends GetxController {
|
||||
BoxName.nameDriver,
|
||||
'${(d['first_name'])}'
|
||||
' ${(d['last_name'])}');
|
||||
if ((d['model'].toString().contains('دراجه') ||
|
||||
d['make'].toString().contains('دراجه '))) {
|
||||
if ((d['gender']).toString() == 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2017) {
|
||||
if ((d['gender']).toString() != 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||
int.parse(d['year'].toString()) < 2017) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
updateAppTester(AppInformation.appName);
|
||||
|
||||
var fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||
|
||||
var token = await CRUD().get(
|
||||
link: AppLink.getDriverToken,
|
||||
payload: {'captain_id': box.read(BoxName.driverID).toString()});
|
||||
|
||||
if (token != 'failure') {
|
||||
if ((jsonDecode(token)['data'][0]['token']) !=
|
||||
(box.read(BoxName.tokenDriver))) {
|
||||
// Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
||||
// 'token change'.tr,
|
||||
// 'change device'.tr,
|
||||
// (jsonDecode(token)['data'][0]['token']).toString(),
|
||||
// [],
|
||||
// 'ding.wav');
|
||||
NotificationService.sendNotification(
|
||||
target: (jsonDecode(token)['data'][0]['token']).toString(),
|
||||
title: 'token change'.tr,
|
||||
body: 'token change'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'cancel',
|
||||
driverList: [], category: 'token change',
|
||||
);
|
||||
Get.defaultDialog(
|
||||
title: 'you will use this device?'.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
await CRUD()
|
||||
.post(link: AppLink.addTokensDriver, payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id': box.read(BoxName.driverID).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
});
|
||||
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Get.off(() => HomeCaptain());
|
||||
// Get.off(() => LoginCaptin());
|
||||
} else {
|
||||
Get.offAll(() => SmsSignupEgypt());
|
||||
|
||||
mySnackeBarError('Login failed'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('');
|
||||
|
||||
mySnackeBarError('Server error'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('Network error'.tr);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ class PhoneAuthHelper {
|
||||
// Define your server URLs
|
||||
static final String _sendOtpUrl = '${AppLink.server}/auth/otp/request.php';
|
||||
static final String _verifyOtpUrl = '${AppLink.server}/auth/otp/verify.php';
|
||||
static final String _registerUrl = '${AppLink.server}/auth/syria/driver/register_driver.php';
|
||||
static final String _registerUrl =
|
||||
'${AppLink.server}/auth/syria/driver/register_driver.php';
|
||||
// removed formatSyrianPhone
|
||||
|
||||
/// Sends an OTP to the provided phone number.
|
||||
@@ -89,7 +90,7 @@ class PhoneAuthHelper {
|
||||
box.write(BoxName.driverID, driver['id']);
|
||||
box.write(BoxName.emailDriver, driver['email']);
|
||||
|
||||
await Get.find<LoginDriverController>().loginWithGoogleCredential(
|
||||
await Get.find<LoginDriverController>().loginDriver(
|
||||
driver['id'].toString(), driver['email'].toString());
|
||||
} else {
|
||||
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
|
||||
|
||||
@@ -113,7 +113,7 @@ class RegisterCaptainController extends GetxController {
|
||||
backgroundColor: AppColor.greenColor);
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
box.write(BoxName.phone, ('+2${phoneController.text}'));
|
||||
await Get.put(LoginDriverController()).loginWithGoogleCredential(
|
||||
await Get.put(LoginDriverController()).loginDriver(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
(box.read(BoxName.emailDriver).toString()),
|
||||
);
|
||||
@@ -229,7 +229,7 @@ class RegisterCaptainController extends GetxController {
|
||||
mySnackbarSuccess('Phone number is already verified'.tr);
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
box.write(BoxName.phone, ('+2${phoneController.text}'));
|
||||
Get.put(LoginDriverController()).loginWithGoogleCredential(
|
||||
Get.put(LoginDriverController()).loginDriver(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.emailDriver).toString(),
|
||||
);
|
||||
|
||||
@@ -371,7 +371,8 @@ class RegistrationController extends GetxController {
|
||||
data.forEach((k, v) => request.fields[k] = v);
|
||||
|
||||
// المهلة الزمنية 120 ثانية لتناسب الاتصالات الضعيفة
|
||||
final streamed = await request.send().timeout(const Duration(seconds: 120));
|
||||
final streamed =
|
||||
await request.send().timeout(const Duration(seconds: 120));
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
@@ -469,7 +470,8 @@ class RegistrationController extends GetxController {
|
||||
}
|
||||
|
||||
final statusOk = j['status'] == 'success';
|
||||
final fileUrl = (j['file_url'] ?? j['message']?['file_url'])?.toString();
|
||||
final fileUrl =
|
||||
(j['file_url'] ?? j['message']?['file_url'])?.toString();
|
||||
|
||||
if (resp.statusCode == 200 &&
|
||||
statusOk &&
|
||||
@@ -512,14 +514,23 @@ class RegistrationController extends GetxController {
|
||||
|
||||
final isSyria = box.read(BoxName.countryCode) == 'Syria';
|
||||
|
||||
if (idFrontUrl == null || idFrontUrl.isEmpty ||
|
||||
idBackUrl == null || idBackUrl.isEmpty ||
|
||||
driverLicenseUrl == null || driverLicenseUrl.isEmpty ||
|
||||
(isSyria && (driverLicenseBackUrl == null || driverLicenseBackUrl.isEmpty)) ||
|
||||
profilePicUrl == null || profilePicUrl.isEmpty ||
|
||||
carFrontUrl == null || carFrontUrl.isEmpty ||
|
||||
carBackUrl == null || carBackUrl.isEmpty) {
|
||||
mySnackbarWarning('Please wait for all documents to finish uploading before registering.'.tr);
|
||||
if (idFrontUrl == null ||
|
||||
idFrontUrl.isEmpty ||
|
||||
idBackUrl == null ||
|
||||
idBackUrl.isEmpty ||
|
||||
driverLicenseUrl == null ||
|
||||
driverLicenseUrl.isEmpty ||
|
||||
(isSyria &&
|
||||
(driverLicenseBackUrl == null || driverLicenseBackUrl.isEmpty)) ||
|
||||
profilePicUrl == null ||
|
||||
profilePicUrl.isEmpty ||
|
||||
carFrontUrl == null ||
|
||||
carFrontUrl.isEmpty ||
|
||||
carBackUrl == null ||
|
||||
carBackUrl.isEmpty) {
|
||||
mySnackbarWarning(
|
||||
'Please wait for all documents to finish uploading before registering.'
|
||||
.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -626,7 +637,8 @@ class RegistrationController extends GetxController {
|
||||
_addField(fields, 'id_front', idFrontUrl);
|
||||
_addField(fields, 'id_back', idBackUrl);
|
||||
_addField(fields, 'driver_license', driverLicenseUrl);
|
||||
if (isSyria) _addField(fields, 'driver_license_back', driverLicenseBackUrl);
|
||||
if (isSyria)
|
||||
_addField(fields, 'driver_license_back', driverLicenseBackUrl);
|
||||
_addField(fields, 'profile_picture', profilePicUrl);
|
||||
_addField(fields, 'criminal_record', criminalRecordUrl);
|
||||
_addField(fields, 'car_license_front', carFrontUrl);
|
||||
@@ -671,7 +683,7 @@ class RegistrationController extends GetxController {
|
||||
final c = Get.isRegistered<LoginDriverController>()
|
||||
? Get.find<LoginDriverController>()
|
||||
: Get.put(LoginDriverController());
|
||||
c.loginWithGoogleCredential(driverID, email);
|
||||
c.loginDriver(driverID, email);
|
||||
} else {
|
||||
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||||
mySnackeBarError(msg);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/country_polygons.dart';
|
||||
import 'package:siro_driver/constant/city_polygons.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import '../../print.dart';
|
||||
|
||||
class CountryLogic {
|
||||
/// Formats the phone number based on the country's dialing rules.
|
||||
@@ -91,4 +97,99 @@ class CountryLogic {
|
||||
final country = box.read(BoxName.countryCode) ?? 'Syria';
|
||||
return formatPhone(cleanPhone, country);
|
||||
}
|
||||
|
||||
/// التحقق مما إذا كانت نقطة داخل مضلع (Ray Casting Algorithm)
|
||||
static bool isPointInPolygon(LatLng point, List<LatLng> polygon) {
|
||||
int intersections = 0;
|
||||
for (int i = 0; i < polygon.length; i++) {
|
||||
final v1 = polygon[i];
|
||||
final v2 = polygon[(i + 1) % polygon.length];
|
||||
|
||||
if ((v1.latitude <= point.latitude && v2.latitude > point.latitude) ||
|
||||
(v2.latitude <= point.latitude && v1.latitude > point.latitude)) {
|
||||
final t = (point.latitude - v1.latitude) / (v2.latitude - v1.latitude);
|
||||
if (point.longitude <
|
||||
v1.longitude + t * (v2.longitude - v1.longitude)) {
|
||||
intersections++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return intersections % 2 != 0;
|
||||
}
|
||||
|
||||
/// كشف الدولة تلقائياً عبر الموقع الجغرافي
|
||||
/// باستخدام مضلعات الدول المحددة في CountryPolygons
|
||||
/// تعيد 'Syria' | 'Egypt' | 'Jordan' | null (في حال الفشل)
|
||||
static Future<String?> detectByLocation() async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
Log.print('[CountryLogic] GPS غير مفعل');
|
||||
return null;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
Log.print('[CountryLogic] رفض الإذن');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (permission == LocationPermission.deniedForever) return null;
|
||||
|
||||
final pos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.low,
|
||||
timeLimit: const Duration(seconds: 8),
|
||||
);
|
||||
|
||||
final point = LatLng(pos.latitude, pos.longitude);
|
||||
|
||||
// فحص المضلعات بالترتيب: Jordan → Syria → Egypt
|
||||
if (isPointInPolygon(point, CountryPolygons.jordanBoundary)) {
|
||||
return 'Jordan';
|
||||
}
|
||||
if (isPointInPolygon(point, CountryPolygons.syriaBoundary)) {
|
||||
return 'Syria';
|
||||
}
|
||||
if (isPointInPolygon(point, CountryPolygons.egyptBoundary)) {
|
||||
return 'Egypt';
|
||||
}
|
||||
|
||||
Log.print('[CountryLogic] الموقع خارج جميع المضلعات');
|
||||
} catch (e) {
|
||||
Log.print('[CountryLogic] detectByLocation error: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// تهيئة الدولة عند بدء التطبيق
|
||||
/// إذا لم تكن الدولة محددة مسبقاً → يكشفها عبر الموقع
|
||||
/// إذا فشل الكشف → Jordan كخيار افتراضي
|
||||
static Future<void> initializeCountry() async {
|
||||
final existing = box.read(BoxName.countryCode);
|
||||
if (existing != null && existing.toString().isNotEmpty) {
|
||||
Log.print('[CountryLogic] الدولة موجودة مسبقاً: $existing');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.print('[CountryLogic] كشف الدولة عبر الموقع...');
|
||||
final country = await detectByLocation();
|
||||
|
||||
if (country != null) {
|
||||
box.write(BoxName.countryCode, country);
|
||||
Log.print('[CountryLogic] تم الكشف: $country');
|
||||
} else {
|
||||
box.write(BoxName.countryCode, 'Jordan');
|
||||
Log.print('[CountryLogic] افتراضي: Jordan');
|
||||
}
|
||||
}
|
||||
|
||||
/// إرجاع مضلع المدينة بناءً على الدولة الحالية
|
||||
static List<LatLng> getCurrentCityPolygon() {
|
||||
final country = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||
if (country == 'Syria') return CityPolygons.damascusPolygon;
|
||||
if (country == 'Egypt') return CityPolygons.cairoPolygon;
|
||||
return CityPolygons.ammanPolygon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ class AI extends GetxController {
|
||||
// DeviceController().getDeviceSerialNumber();
|
||||
box.write(BoxName.phoneVerified, true);
|
||||
|
||||
Get.find<LoginDriverController>().loginWithGoogleCredential(
|
||||
Get.find<LoginDriverController>().loginDriver(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.emailDriver).toString(),
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ class SmsEgyptController extends GetxController {
|
||||
('+2${Get.find<RegisterCaptainController>().phoneController.text}'));
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
|
||||
await Get.put(LoginDriverController()).loginWithGoogleCredential(
|
||||
await Get.put(LoginDriverController()).loginDriver(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
(box.read(BoxName.emailDriver).toString()),
|
||||
);
|
||||
|
||||
@@ -130,7 +130,7 @@ class SplashScreenController extends GetxController
|
||||
await initializer.initializeApp();
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
await loginController.loginWithGoogleCredential(
|
||||
await loginController.loginDriver(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.emailDriver).toString(),
|
||||
);
|
||||
|
||||
@@ -118,7 +118,7 @@ class AuthScreen extends StatelessWidget {
|
||||
const Text('Login', style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
if (testerFormKey.currentState!.validate()) {
|
||||
controller.logintest(
|
||||
controller.loginUsingCredentialsWithoutGoogle(
|
||||
testerPasswordController.text.trim(),
|
||||
testerEmailController.text.trim(),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import '../../../../constant/api_key.dart';
|
||||
import '../../../../controller/functions/location_controller.dart';
|
||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||
import '../../../../controller/functions/country_logic.dart';
|
||||
|
||||
class OsmMapView extends StatelessWidget {
|
||||
const OsmMapView({super.key});
|
||||
@@ -22,27 +23,77 @@ class OsmMapView extends StatelessWidget {
|
||||
locationController.myLocation.longitude);
|
||||
final double currentHeading = locationController.heading;
|
||||
|
||||
return IntaleqMap(
|
||||
apiKey: AK.mapSaasKey,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: currentLocation,
|
||||
zoom: 15,
|
||||
bearing: currentHeading,
|
||||
),
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('myLocation'),
|
||||
position: currentLocation,
|
||||
rotation: currentHeading,
|
||||
icon: homeCaptainController.carIcon,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 2,
|
||||
)
|
||||
},
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
// You can assign this controller if needed
|
||||
},
|
||||
final cityPolygon = CountryLogic.getCurrentCityPolygon();
|
||||
final bool inServiceArea = CountryLogic.isPointInPolygon(currentLocation, cityPolygon);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
IntaleqMap(
|
||||
apiKey: AK.mapSaasKey,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: currentLocation,
|
||||
zoom: 15,
|
||||
bearing: currentHeading,
|
||||
),
|
||||
markers: {
|
||||
Marker(
|
||||
markerId: const MarkerId('myLocation'),
|
||||
position: currentLocation,
|
||||
rotation: currentHeading,
|
||||
icon: homeCaptainController.carIcon,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 2,
|
||||
)
|
||||
},
|
||||
polygons: {
|
||||
Polygon(
|
||||
polygonId: const PolygonId('city_polygon'),
|
||||
points: cityPolygon,
|
||||
fillColor: Colors.green.withOpacity(0.15),
|
||||
strokeColor: Colors.green[800]!,
|
||||
strokeWidth: 2,
|
||||
)
|
||||
},
|
||||
onMapCreated: (IntaleqMapController controller) {
|
||||
// You can assign this controller if needed
|
||||
},
|
||||
),
|
||||
if (!inServiceArea)
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.redAccent.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black26, blurRadius: 4, offset: Offset(0, 2))
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'الخدمة غير متوفرة في منطقتك حالياً',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -398,6 +398,7 @@ static String get updateTesterApp => "$auth/Tester/updateTesterApp.php";
|
||||
static String get signUp => "$auth/signup.php";
|
||||
static String get sendVerifyEmail => "$auth/sendVerifyEmail.php";
|
||||
static String get loginFromGooglePassenger => "$auth/loginFromGooglePassenger.php";
|
||||
static String get loginUsingCredentialsWithoutGooglePassenger => "$auth/loginUsingCredentialsWithoutGooglePassenger.php";
|
||||
static String get checkPhoneNumberISVerfiedPassenger =>
|
||||
"$auth/checkPhoneNumberISVerfiedPassenger.php";
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:location/location.dart';
|
||||
import '../../env/env.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/auth/otp_token_page.dart';
|
||||
import '../functions/country_logic.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
import '../functions/package_info.dart';
|
||||
|
||||
@@ -48,7 +49,8 @@ class LoginController extends GetxController {
|
||||
@override
|
||||
void onInit() async {
|
||||
// Log.print('box.read(BoxName.isTest): ${box.read(BoxName.isTest)}');
|
||||
box.write(BoxName.countryCode, 'Syria');
|
||||
// كشف الدولة تلقائياً عبر الموقع الجغرافي
|
||||
await CountryLogic.initializeCountry();
|
||||
FirebaseMessagesController().getToken();
|
||||
super.onInit();
|
||||
}
|
||||
@@ -271,8 +273,6 @@ class LoginController extends GetxController {
|
||||
final res = await CRUD().get(
|
||||
link: AppLink.loginFromGooglePassenger,
|
||||
payload: {
|
||||
'email': email,
|
||||
'id': passengerID, // استخدم المعامل مباشرة
|
||||
'platform': Platform.isAndroid ? 'android' : 'ios',
|
||||
'appName': AppInformation.appName,
|
||||
},
|
||||
@@ -385,7 +385,7 @@ class LoginController extends GetxController {
|
||||
"inviteCode": invite,
|
||||
"passengerID": passengerID,
|
||||
});
|
||||
|
||||
|
||||
// سجل الدعوة أيضاً في النظام الموحد الجديد
|
||||
await CRUD().post(link: AppLink.addUnifiedInvite, payload: {
|
||||
"inviter_code": invite,
|
||||
@@ -445,46 +445,48 @@ class LoginController extends GetxController {
|
||||
// );
|
||||
// }
|
||||
|
||||
void login() async {
|
||||
void loginTester() async {
|
||||
isloading = true;
|
||||
update();
|
||||
var res =
|
||||
await CRUD().get(link: AppLink.loginFromGooglePassenger, payload: {
|
||||
'email': (emailController.text),
|
||||
'id': passwordController.text,
|
||||
"platform": Platform.isAndroid ? 'android' : 'ios',
|
||||
"appName": AppInformation.appName,
|
||||
});
|
||||
|
||||
try {
|
||||
var fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
var payload = {
|
||||
'email': emailController.text.trim(),
|
||||
'password': passwordController.text.trim(),
|
||||
'fingerPrint': fingerPrint,
|
||||
'aud': '${AK.allowed}${Platform.isAndroid ? 'android' : 'ios'}',
|
||||
};
|
||||
|
||||
var response = await http.post(
|
||||
Uri.parse(AppLink.loginUsingCredentialsWithoutGooglePassenger),
|
||||
body: payload,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
var jsonDecoeded = jsonDecode(response.body);
|
||||
if (jsonDecoeded['status'] == 'success' && jsonDecoeded['data'][0]['verified'].toString() == '1') {
|
||||
var d = jsonDecoeded['data'][0];
|
||||
var jwt = jsonDecoeded['jwt'];
|
||||
|
||||
// حفظ التوكن
|
||||
if (jwt != null) {
|
||||
box.write(BoxName.jwt, c(jwt));
|
||||
await storage.write(key: BoxName.jwt, value: c(jwt));
|
||||
}
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
if (res == 'Failure') {
|
||||
//Failure
|
||||
Get.offAll(() => LoginPage());
|
||||
isloading = false;
|
||||
update();
|
||||
// Get.snackbar("User does not exist.".tr, '', backgroundColor: Colors.red);
|
||||
} else {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success' &&
|
||||
jsonDecoeded['data'][0]['verified'].toString() == '1') {
|
||||
//
|
||||
box.write(BoxName.isVerified, '1');
|
||||
box.write(BoxName.email, jsonDecoeded['data'][0]['email']);
|
||||
box.write(BoxName.name, jsonDecoeded['data'][0]['first_name']);
|
||||
box.write(BoxName.phone, jsonDecoeded['data'][0]['phone']);
|
||||
box.write(BoxName.passengerID, passwordController.text);
|
||||
// var token = await CRUD().get(link: AppLink.getTokens, payload: {
|
||||
// 'passengerID': box.read(BoxName.passengerID).toString()
|
||||
// });
|
||||
// await updateAppTester(AppInformation.appName);
|
||||
box.write(BoxName.email, d['email']);
|
||||
box.write(BoxName.name, d['first_name']);
|
||||
box.write(BoxName.phone, d['phone']);
|
||||
box.write(BoxName.passengerID, d['id']);
|
||||
box.write(BoxName.isTest, '1');
|
||||
|
||||
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||
|
||||
Get.offAll(() => const MapPagePassenger());
|
||||
} else {
|
||||
// Get.offAll(() => SmsSignupEgypt());
|
||||
// Get.snackbar(jsonDecoeded['status'], jsonDecoeded['data'],
|
||||
// backgroundColor: Colors.redAccent);
|
||||
Get.offAll(() => LoginPage());
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
@@ -492,6 +494,10 @@ class LoginController extends GetxController {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Tester Login Error: $e");
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/country_polygons.dart';
|
||||
import 'package:siro_rider/main.dart';
|
||||
import '../../print.dart';
|
||||
|
||||
class CountryLogic {
|
||||
/// Formats the phone number based on the country's dialing rules.
|
||||
@@ -88,7 +92,77 @@ class CountryLogic {
|
||||
return formatPhone(cleanPhone, 'Jordan');
|
||||
}
|
||||
|
||||
final country = box.read(BoxName.countryCode) ?? 'Syria';
|
||||
final country = box.read(BoxName.countryCode) ?? 'Jordan';
|
||||
return formatPhone(cleanPhone, country);
|
||||
}
|
||||
|
||||
/// التحقق مما إذا كانت نقطة داخل مضلع (Ray Casting Algorithm)
|
||||
static bool _isPointInPolygon(LatLng point, List<LatLng> polygon) {
|
||||
int intersections = 0;
|
||||
for (int i = 0; i < polygon.length; i++) {
|
||||
final v1 = polygon[i];
|
||||
final v2 = polygon[(i + 1) % polygon.length];
|
||||
if ((v1.latitude <= point.latitude && v2.latitude > point.latitude) ||
|
||||
(v2.latitude <= point.latitude && v1.latitude > point.latitude)) {
|
||||
final t = (point.latitude - v1.latitude) / (v2.latitude - v1.latitude);
|
||||
if (point.longitude <
|
||||
v1.longitude + t * (v2.longitude - v1.longitude)) {
|
||||
intersections++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return intersections % 2 != 0;
|
||||
}
|
||||
|
||||
/// كشف الدولة تلقائياً عبر الموقع الجغرافي باستخدام مضلعات CountryPolygons
|
||||
static Future<String?> detectByLocation() async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) return null;
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) return null;
|
||||
}
|
||||
if (permission == LocationPermission.deniedForever) return null;
|
||||
|
||||
final pos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.low,
|
||||
timeLimit: const Duration(seconds: 8),
|
||||
);
|
||||
final point = LatLng(pos.latitude, pos.longitude);
|
||||
|
||||
if (_isPointInPolygon(point, CountryPolygons.jordanBoundary)) {
|
||||
return 'Jordan';
|
||||
}
|
||||
if (_isPointInPolygon(point, CountryPolygons.syriaBoundary)) {
|
||||
return 'Syria';
|
||||
}
|
||||
if (_isPointInPolygon(point, CountryPolygons.egyptBoundary)) {
|
||||
return 'Egypt';
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('[CountryLogic] detectByLocation error: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// تهيئة الدولة عند بدء التطبيق
|
||||
static Future<void> initializeCountry() async {
|
||||
final existing = box.read(BoxName.countryCode);
|
||||
if (existing != null && existing.toString().isNotEmpty) {
|
||||
Log.print('[CountryLogic] الدولة موجودة مسبقاً: $existing');
|
||||
return;
|
||||
}
|
||||
Log.print('[CountryLogic] كشف الدولة عبر الموقع...');
|
||||
final country = await detectByLocation();
|
||||
if (country != null) {
|
||||
box.write(BoxName.countryCode, country);
|
||||
Log.print('[CountryLogic] تم الكشف: $country');
|
||||
} else {
|
||||
box.write(BoxName.countryCode, 'Jordan');
|
||||
Log.print('[CountryLogic] افتراضي: Jordan');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,14 +75,39 @@ class CRUD {
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// دالة مساعدة خاصة: يجيب البصمة المشفرة من GetStorage
|
||||
// هي نفس القيمة المرسلة في login وعُملها hash في JWT payload
|
||||
// السيرفر يعمل: sha256(X-Device-FP + FP_PEPPER) == JWT.fingerPrint
|
||||
// دالة مساعدة خاصة: تجيب البصمة المشفرة من GetStorage
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
String _getFpHeader() {
|
||||
return box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// دالة مساعدة خاصة: تقرأ JWT من FlutterSecureStorage (آمن)
|
||||
// بدلاً من GetStorage (غير مشفر)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
Future<String> _getJwt() async {
|
||||
try {
|
||||
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
|
||||
if (encryptedJwt == null || encryptedJwt.isEmpty) {
|
||||
// Fallback إلى GetStorage للتوافقية
|
||||
final String? fallback = box.read(BoxName.jwt);
|
||||
if (fallback != null) {
|
||||
return r(fallback).toString().split(Env.addd)[0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return r(encryptedJwt).toString().split(Env.addd)[0];
|
||||
} catch (e) {
|
||||
Log.print('Error reading JWT from SecureStorage: $e');
|
||||
// Fallback
|
||||
final String? fallback = box.read(BoxName.jwt);
|
||||
if (fallback != null) {
|
||||
return r(fallback).toString().split(Env.addd)[0];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Centralized private method to handle all API requests.
|
||||
/// Includes retry logic, network checking, and standardized error handling.
|
||||
Future<dynamic> _makeRequest({
|
||||
@@ -169,7 +194,7 @@ class CRUD {
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
final token = await _getJwt();
|
||||
|
||||
final headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
@@ -193,14 +218,14 @@ class CRUD {
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
final token = await _getJwt();
|
||||
var url = Uri.parse(link);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization':
|
||||
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
},
|
||||
);
|
||||
@@ -548,7 +573,6 @@ class CRUD {
|
||||
return response.statusCode;
|
||||
}
|
||||
|
||||
|
||||
Future<dynamic> postPayMob({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
|
||||
@@ -4,12 +4,9 @@ import 'package:siro_rider/constant/box_name.dart';
|
||||
import 'package:siro_rider/constant/info.dart';
|
||||
import 'package:siro_rider/controller/auth/login_controller.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
|
||||
import '../../constant/char_map.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import 'crud.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
@@ -24,23 +21,25 @@ class SecureStorage {
|
||||
}
|
||||
}
|
||||
|
||||
const List<String> keysToFetch = [
|
||||
'serverPHP',
|
||||
// 'seferAlexandriaServer',
|
||||
// 'seferPaymentServer',
|
||||
// 'seferCairoServer',
|
||||
// 'seferGizaServer',
|
||||
];
|
||||
|
||||
class AppInitializer {
|
||||
List<Map<String, dynamic>> links = [];
|
||||
|
||||
/// تقرأ JWT من FlutterSecureStorage أولاً، مع fallback إلى GetStorage
|
||||
Future<String?> _readJwt() async {
|
||||
try {
|
||||
final secureJwt = await storage.read(key: 'jwt');
|
||||
if (secureJwt != null && secureJwt.isNotEmpty) return secureJwt;
|
||||
} catch (_) {}
|
||||
return box.read(BoxName.jwt);
|
||||
}
|
||||
|
||||
Future<void> initializeApp() async {
|
||||
if (box.read(BoxName.jwt) == null) {
|
||||
final jwt = await _readJwt();
|
||||
if (jwt == null) {
|
||||
await LoginController().getJWT();
|
||||
} else {
|
||||
bool isTokenExpired = JwtDecoder.isExpired(
|
||||
r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0]);
|
||||
bool isTokenExpired =
|
||||
JwtDecoder.isExpired(r(jwt).toString().split(AppInformation.addd)[0]);
|
||||
|
||||
if (isTokenExpired) {
|
||||
await LoginController().getJWT();
|
||||
|
||||
@@ -419,7 +419,7 @@ class LoginPage extends StatelessWidget {
|
||||
onPressed: () {
|
||||
if (controller.formKey.currentState!
|
||||
.validate()) {
|
||||
controller.login();
|
||||
controller.loginTester();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -160,7 +160,7 @@ class AuthScreen extends StatelessWidget {
|
||||
testerEmailController.text;
|
||||
controller.passwordController.text =
|
||||
testerPasswordController.text;
|
||||
controller.login();
|
||||
controller.loginTester();
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user