Update: 2026-06-12 22:40:40

This commit is contained in:
Hamza-Ayed
2026-06-12 22:40:40 +03:00
parent f907212c57
commit 0ae368dbc8
24 changed files with 1197 additions and 303 deletions

View File

@@ -4,19 +4,11 @@ require_once __DIR__ . '/../../connect.php';
try { try {
/* ──────────────────────────────── /* ────────────────────────────────
1) قراءة القيم الأولية 1) استخدام ID من التوكن (JWT)
───────────────────────────────── */ ───────────────────────────────── */
// $emailRaw = filterRequest('email'); // البريد القادم من التطبيق (غير مشفَّر) $driverID = $user_id;
$driverID = filterRequest('id'); // DriverID المُرسل
// error_log("[Debug] Email (raw): $emailRaw"); error_log("[Debug] DriverID from JWT: $driverID");
error_log("[Debug] DriverID: $driverID");
/* ────────────────────────────────
2) تشفير الإيميل
───────────────────────────────── */
// $emailEnc = $encryptionHelper->encryptData($emailRaw);
// error_log("[Debug] Email (encrypted): $emailEnc");
/* ──────────────────────────────── /* ────────────────────────────────
3) إعداد الاستعلام الموحَّد 3) إعداد الاستعلام الموحَّد
@@ -36,7 +28,6 @@ try {
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
LEFT JOIN invites inv ON inv.driverId = driver.id LEFT JOIN invites inv ON inv.driverId = driver.id
WHERE WHERE
driver.id = :id driver.id = :id
-- AND phone_verification.is_verified = '1' -- AND phone_verification.is_verified = '1'
LIMIT 1 LIMIT 1
@@ -48,7 +39,6 @@ try {
// باراميترات الربط // باراميترات الربط
$params = [ $params = [
//':email' => $emailEnc,
':id' => $driverID, ':id' => $driverID,
]; ];
foreach ($params as $k => $v) { foreach ($params as $k => $v) {

View File

@@ -1,77 +1,94 @@
<?php <?php
// ============================================================
// loginUsingCredentialsWithoutGoogle.php
// مخصص لدخول الفاحصين (Testers) بالإيميل والباسورد
// ============================================================
require_once __DIR__ . '/../../connect.php'; require_once __DIR__ . '/../../core/bootstrap.php';
$email = filterRequest('email'); $email = filterRequest('email');
$password = filterRequest('password'); $password = filterRequest('password');
$audience = filterRequest('aud') ?? 'siro-driver-android'; // الافتراضي
$fingerprint = filterRequest('fingerPrint') ?? filterRequest('fingerprint');
// تشفير الإيميل لاستخدامه في الاستعلام // تشفير الإيميل لاستخدامه في الاستعلام
$encryptedEmail = $encryptionHelper->encryptData($email); $encryptedEmail = $encryptionHelper->encryptData($email);
// SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر try {
$sql = "SELECT $con = Database::get('main');
driver.id,
driver.phone, // SQL لاسترجاع المستخدم بناءً على البريد الإلكتروني المشفر
driver.email, $sql = "SELECT
driver.gender, driver.id,
driver.birthdate, driver.phone,
driver.site, driver.email,
driver.first_name, driver.gender,
driver.last_name, driver.birthdate,
driver.bankCode, driver.site,
driver.accountBank, driver.first_name,
driver.education, driver.last_name,
driver.employmentType, driver.bankCode,
driver.maritalStatus, driver.accountBank,
driver.created_at, driver.employmentType,
driver.updated_at, driver.maritalStatus,
driver.password, driver.created_at,
phone_verification.is_verified, driver.updated_at,
CarRegistration.make, driver.password,
CarRegistration.model, phone_verification.is_verified,
CarRegistration.year CarRegistration.make,
FROM CarRegistration.model,
driver CarRegistration.year
LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone FROM driver
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id LEFT JOIN phone_verification ON phone_verification.phone_number = driver.phone
WHERE LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
driver.email = :email AND phone_verification.is_verified = '1' WHERE
LIMIT 1"; driver.email = :email
LIMIT 1";
$stmt = $con->prepare($sql); $stmt = $con->prepare($sql);
$stmt->bindParam(':email', $encryptedEmail); $stmt->bindParam(':email', $encryptedEmail);
$stmt->execute(); $stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC); $data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) { if ($data) {
if (password_verify($password, $data['password'])) { // فحص الباسورد (في نظامنا، يمكن أن يكون الباسورد هو HMAC أو نص عادي للفاحصين)
unset($data['password']); // لنفترض أن الفاحص له باسورد عادي أو مشفر بـ bcrypt
if (password_verify($password, $data['password']) || $password === $data['password']) {
unset($data['password']);
// فك تشفير الحقول الحساسة // فك تشفير الحقول الحساسة
$data['phone'] = $encryptionHelper->decryptData($data['phone']); $data['phone'] = $encryptionHelper->decryptData($data['phone']);
$data['email'] = $encryptionHelper->decryptData($data['email']); $data['email'] = $encryptionHelper->decryptData($data['email']);
$data['gender'] = $encryptionHelper->decryptData($data['gender']); $data['gender'] = $encryptionHelper->decryptData($data['gender']);
$data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']); $data['birthdate'] = $encryptionHelper->decryptData($data['birthdate']);
$data['site'] = $encryptionHelper->decryptData($data['site']); $data['site'] = $encryptionHelper->decryptData($data['site']);
$data['first_name'] = $encryptionHelper->decryptData($data['first_name']); $data['first_name'] = $encryptionHelper->decryptData($data['first_name']);
$data['last_name'] = $encryptionHelper->decryptData($data['last_name']); $data['last_name'] = $encryptionHelper->decryptData($data['last_name']);
$data['education'] = $encryptionHelper->decryptData($data['education']); if(isset($data['employmentType'])) $data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
$data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']); if(isset($data['maritalStatus'])) $data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
$data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
echo json_encode([ // توليد الـ JWT بصلاحية (tester) لتميزهم عن السائقين الفعليين
"status" => "success", $jwtService = new JwtService($redis);
"data" => $data $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 { } else {
jsonError("Incorrect password."); jsonError("User does not exist.");
} }
} else { } catch (Exception $e) {
jsonError("User does not exist or phone number not verified."); error_log("[Tester Login Error] " . $e->getMessage());
jsonError("Server error occurred.");
} finally {
$stmt = null;
$con = null;
} }
$stmt = null;
$con = null;
exit(); exit();
?> ?>

View File

@@ -2,14 +2,12 @@
require_once __DIR__ . '/../connect.php'; require_once __DIR__ . '/../connect.php';
// استدعاء المعاملات // لا نستقبل id أو email من التطبيق بل نأخذهم من التوكن (JWT) لزيادة الأمان
$email = filterRequest('email');
$id = filterRequest('id');
$platform = filterRequest("platform") ?: 'unknown'; $platform = filterRequest("platform") ?: 'unknown';
$appName = filterRequest("appName") ?: 'unknown'; $appName = filterRequest("appName") ?: 'unknown';
// تشفير الإيميل لأنه يُرسل من التطبيق غير مشفّر // الاعتماد كلياً على الـ ID المستخرج من JWT داخل connect.php
$email = $encryptionHelper->encryptData($email); $id = $user_id;
// تجهيز الاستعلام // تجهيز الاستعلام
$sql = "SELECT $sql = "SELECT
@@ -47,12 +45,11 @@ LEFT JOIN promos
ON promos.passengerID = p.id ON promos.passengerID = p.id
LEFT JOIN tokens t LEFT JOIN tokens t
ON t.passengerID = p.id 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"; LIMIT 1";
// تنفيذ الاستعلام // تنفيذ الاستعلام
$stmt = $con->prepare($sql); $stmt = $con->prepare($sql);
$stmt->bindParam(':email', $email);
$stmt->bindParam(':id', $id); $stmt->bindParam(':id', $id);
$stmt->bindParam(':appName', $appName); $stmt->bindParam(':appName', $appName);
$stmt->bindParam(':platform', $platform); $stmt->bindParam(':platform', $platform);

View 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();

View 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
View 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>

View 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), // العودة لنقطة البداية
];
}

View File

@@ -28,6 +28,7 @@ import '../../../views/auth/syria/pending_driver_page.dart';
import '../../firebase/firbase_messge.dart'; import '../../firebase/firbase_messge.dart';
import '../../firebase/local_notification.dart'; import '../../firebase/local_notification.dart';
import '../../firebase/notification_service.dart'; import '../../firebase/notification_service.dart';
import '../../functions/country_logic.dart';
import '../../functions/encrypt_decrypt.dart'; import '../../functions/encrypt_decrypt.dart';
import '../../functions/package_info.dart'; import '../../functions/package_info.dart';
import '../../functions/secure_storage.dart'; import '../../functions/secure_storage.dart';
@@ -66,38 +67,39 @@ class LoginDriverController extends GetxController {
@override @override
void onInit() async { void onInit() async {
box.write(BoxName.countryCode, 'Syria'); // كشف الدولة تلقائياً عبر الموقع الجغرافي
await CountryLogic.initializeCountry();
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b'); // box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
// await getAppTester(); // await getAppTester();
getJWT(); getJWT();
super.onInit(); super.onInit();
} }
getAppTester() async { // getAppTester() async {
var res = await CRUD().get( // var res = await CRUD().get(
link: AppLink.getTesterApp, // link: AppLink.getTesterApp,
payload: {'appPlatform': AppInformation.appName}); // payload: {'appPlatform': AppInformation.appName});
// Log.print('res: ${res}'); // // Log.print('res: ${res}');
if (res != 'failure') { // if (res != 'failure') {
var d = jsonDecode(res); // var d = jsonDecode(res);
isTest = d['message'][0]['isTest']; // isTest = d['message'][0]['isTest'];
// Log.print('isTest: ${isTest}'); // // Log.print('isTest: ${isTest}');
box.write(BoxName.isTest, isTest); // box.write(BoxName.isTest, isTest);
// Log.print('isTest: ${box.read(BoxName.isTest)}'); // // Log.print('isTest: ${box.read(BoxName.isTest)}');
update(); // update();
} else { // } else {
isTest = 0; // isTest = 0;
box.write(BoxName.isTest, isTest); // box.write(BoxName.isTest, isTest);
update(); // update();
return false; // return false;
} // }
} // }
updateAppTester(String appPlatform) async { // updateAppTester(String appPlatform) async {
await CRUD().post( // await CRUD().post(
link: AppLink.updateTesterApp, payload: {'appPlatform': appPlatform}); // link: AppLink.updateTesterApp, payload: {'appPlatform': appPlatform});
} // }
isPhoneVerified() async { isPhoneVerified() async {
var res = await CRUD().post( 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; isloading = true;
update(); update();
// await SecurityHelper.performSecurityChecks(); // await SecurityHelper.performSecurityChecks();
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}'); // Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
// await getJWT(); // await getJWT();
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: { var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {});
// 'email': email ?? 'yet', Log.print('loginDriver: ${res}');
'id': driverID,
});
Log.print('loginWithGoogleCredential: ${res}');
if (res == 'failure') { if (res == 'failure') {
await isPhoneVerified(); await isPhoneVerified();
isloading = false; // <--- أضفت هذا أيضاً isloading = false; // <--- أضفت هذا أيضاً
@@ -478,10 +477,7 @@ class LoginDriverController extends GetxController {
// await SecurityHelper.performSecurityChecks(); // await SecurityHelper.performSecurityChecks();
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}'); // Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: { var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {});
'email': email ?? 'yet',
'id': driverID,
});
// print('res is $res'); // print('res is $res');
// if (res == 'failure') { // if (res == 'failure') {
@@ -563,32 +559,34 @@ class LoginDriverController extends GetxController {
loginUsingCredentialsWithoutGoogle(String password, email) async { loginUsingCredentialsWithoutGoogle(String password, email) async {
isloading = true; isloading = true;
isGoogleLogin = true;
update(); 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; try {
update(); var fingerPrint = await DeviceHelper.getDeviceFingerprint();
} else { var payload = {
var jsonDecoeded = jsonDecode(res); 'email': email,
var d = jsonDecoeded['data'][0]; 'password': password,
if (jsonDecoeded.isNotEmpty) { 'fingerPrint': fingerPrint,
if (jsonDecoeded['status'] == 'success' && 'aud': AppInformation.appName,
d['is_verified'].toString() == '1') { };
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.emailDriver, (d['email']));
box.write(BoxName.driverID, (d['id'])); box.write(BoxName.driverID, (d['id']));
box.write(BoxName.isTest, '1'); box.write(BoxName.isTest, '1');
@@ -602,83 +600,24 @@ class LoginDriverController extends GetxController {
BoxName.nameDriver, BoxName.nameDriver,
'${(d['first_name'])}' '${(d['first_name'])}'
' ${(d['last_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); 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(() => HomeCaptain());
// Get.off(() => LoginCaptin());
} else { } else {
Get.offAll(() => SmsSignupEgypt()); mySnackeBarError('Login failed'.tr);
isloading = false; isloading = false;
update(); update();
} }
} else { } else {
mySnackeBarError(''); mySnackeBarError('Server error'.tr);
isloading = false; isloading = false;
update(); update();
} }
} catch (e) {
mySnackeBarError('Network error'.tr);
isloading = false;
update();
} }
} }

View File

@@ -17,7 +17,8 @@ class PhoneAuthHelper {
// Define your server URLs // Define your server URLs
static final String _sendOtpUrl = '${AppLink.server}/auth/otp/request.php'; static final String _sendOtpUrl = '${AppLink.server}/auth/otp/request.php';
static final String _verifyOtpUrl = '${AppLink.server}/auth/otp/verify.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 // removed formatSyrianPhone
/// Sends an OTP to the provided phone number. /// Sends an OTP to the provided phone number.
@@ -89,7 +90,7 @@ class PhoneAuthHelper {
box.write(BoxName.driverID, driver['id']); box.write(BoxName.driverID, driver['id']);
box.write(BoxName.emailDriver, driver['email']); box.write(BoxName.emailDriver, driver['email']);
await Get.find<LoginDriverController>().loginWithGoogleCredential( await Get.find<LoginDriverController>().loginDriver(
driver['id'].toString(), driver['email'].toString()); driver['id'].toString(), driver['email'].toString());
} else { } else {
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل // ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل

View File

@@ -113,7 +113,7 @@ class RegisterCaptainController extends GetxController {
backgroundColor: AppColor.greenColor); backgroundColor: AppColor.greenColor);
box.write(BoxName.phoneVerified, '1'); box.write(BoxName.phoneVerified, '1');
box.write(BoxName.phone, ('+2${phoneController.text}')); 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.driverID).toString(),
(box.read(BoxName.emailDriver).toString()), (box.read(BoxName.emailDriver).toString()),
); );
@@ -229,7 +229,7 @@ class RegisterCaptainController extends GetxController {
mySnackbarSuccess('Phone number is already verified'.tr); mySnackbarSuccess('Phone number is already verified'.tr);
box.write(BoxName.phoneVerified, '1'); box.write(BoxName.phoneVerified, '1');
box.write(BoxName.phone, ('+2${phoneController.text}')); box.write(BoxName.phone, ('+2${phoneController.text}'));
Get.put(LoginDriverController()).loginWithGoogleCredential( Get.put(LoginDriverController()).loginDriver(
box.read(BoxName.driverID).toString(), box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(), box.read(BoxName.emailDriver).toString(),
); );

View File

@@ -371,7 +371,8 @@ class RegistrationController extends GetxController {
data.forEach((k, v) => request.fields[k] = v); data.forEach((k, v) => request.fields[k] = v);
// المهلة الزمنية 120 ثانية لتناسب الاتصالات الضعيفة // المهلة الزمنية 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); final res = await http.Response.fromStream(streamed);
if (res.statusCode != 200) { if (res.statusCode != 200) {
@@ -469,7 +470,8 @@ class RegistrationController extends GetxController {
} }
final statusOk = j['status'] == 'success'; 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 && if (resp.statusCode == 200 &&
statusOk && statusOk &&
@@ -512,14 +514,23 @@ class RegistrationController extends GetxController {
final isSyria = box.read(BoxName.countryCode) == 'Syria'; final isSyria = box.read(BoxName.countryCode) == 'Syria';
if (idFrontUrl == null || idFrontUrl.isEmpty || if (idFrontUrl == null ||
idBackUrl == null || idBackUrl.isEmpty || idFrontUrl.isEmpty ||
driverLicenseUrl == null || driverLicenseUrl.isEmpty || idBackUrl == null ||
(isSyria && (driverLicenseBackUrl == null || driverLicenseBackUrl.isEmpty)) || idBackUrl.isEmpty ||
profilePicUrl == null || profilePicUrl.isEmpty || driverLicenseUrl == null ||
carFrontUrl == null || carFrontUrl.isEmpty || driverLicenseUrl.isEmpty ||
carBackUrl == null || carBackUrl.isEmpty) { (isSyria &&
mySnackbarWarning('Please wait for all documents to finish uploading before registering.'.tr); (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; return;
} }
@@ -626,7 +637,8 @@ class RegistrationController extends GetxController {
_addField(fields, 'id_front', idFrontUrl); _addField(fields, 'id_front', idFrontUrl);
_addField(fields, 'id_back', idBackUrl); _addField(fields, 'id_back', idBackUrl);
_addField(fields, 'driver_license', driverLicenseUrl); _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, 'profile_picture', profilePicUrl);
_addField(fields, 'criminal_record', criminalRecordUrl); _addField(fields, 'criminal_record', criminalRecordUrl);
_addField(fields, 'car_license_front', carFrontUrl); _addField(fields, 'car_license_front', carFrontUrl);
@@ -671,7 +683,7 @@ class RegistrationController extends GetxController {
final c = Get.isRegistered<LoginDriverController>() final c = Get.isRegistered<LoginDriverController>()
? Get.find<LoginDriverController>() ? Get.find<LoginDriverController>()
: Get.put(LoginDriverController()); : Get.put(LoginDriverController());
c.loginWithGoogleCredential(driverID, email); c.loginDriver(driverID, email);
} else { } else {
final msg = (json?['message'] ?? 'Registration failed.').toString(); final msg = (json?['message'] ?? 'Registration failed.').toString();
mySnackeBarError(msg); mySnackeBarError(msg);

View File

@@ -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/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 'package:siro_driver/main.dart';
import '../../print.dart';
class CountryLogic { class CountryLogic {
/// Formats the phone number based on the country's dialing rules. /// Formats the phone number based on the country's dialing rules.
@@ -91,4 +97,99 @@ class CountryLogic {
final country = box.read(BoxName.countryCode) ?? 'Syria'; final country = box.read(BoxName.countryCode) ?? 'Syria';
return formatPhone(cleanPhone, country); 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;
}
} }

View File

@@ -189,7 +189,7 @@ class AI extends GetxController {
// DeviceController().getDeviceSerialNumber(); // DeviceController().getDeviceSerialNumber();
box.write(BoxName.phoneVerified, true); box.write(BoxName.phoneVerified, true);
Get.find<LoginDriverController>().loginWithGoogleCredential( Get.find<LoginDriverController>().loginDriver(
box.read(BoxName.driverID).toString(), box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(), box.read(BoxName.emailDriver).toString(),
); );

View File

@@ -46,7 +46,7 @@ class SmsEgyptController extends GetxController {
('+2${Get.find<RegisterCaptainController>().phoneController.text}')); ('+2${Get.find<RegisterCaptainController>().phoneController.text}'));
box.write(BoxName.phoneVerified, '1'); box.write(BoxName.phoneVerified, '1');
await Get.put(LoginDriverController()).loginWithGoogleCredential( await Get.put(LoginDriverController()).loginDriver(
box.read(BoxName.driverID).toString(), box.read(BoxName.driverID).toString(),
(box.read(BoxName.emailDriver).toString()), (box.read(BoxName.emailDriver).toString()),
); );

View File

@@ -130,7 +130,7 @@ class SplashScreenController extends GetxController
await initializer.initializeApp(); await initializer.initializeApp();
await EncryptionHelper.initialize(); await EncryptionHelper.initialize();
await loginController.loginWithGoogleCredential( await loginController.loginDriver(
box.read(BoxName.driverID).toString(), box.read(BoxName.driverID).toString(),
box.read(BoxName.emailDriver).toString(), box.read(BoxName.emailDriver).toString(),
); );

View File

@@ -118,7 +118,7 @@ class AuthScreen extends StatelessWidget {
const Text('Login', style: TextStyle(color: Colors.white)), const Text('Login', style: TextStyle(color: Colors.white)),
onPressed: () { onPressed: () {
if (testerFormKey.currentState!.validate()) { if (testerFormKey.currentState!.validate()) {
controller.logintest( controller.loginUsingCredentialsWithoutGoogle(
testerPasswordController.text.trim(), testerPasswordController.text.trim(),
testerEmailController.text.trim(), testerEmailController.text.trim(),
); );

View File

@@ -5,6 +5,7 @@ import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../../constant/api_key.dart'; import '../../../../constant/api_key.dart';
import '../../../../controller/functions/location_controller.dart'; import '../../../../controller/functions/location_controller.dart';
import '../../../../controller/home/captin/home_captain_controller.dart'; import '../../../../controller/home/captin/home_captain_controller.dart';
import '../../../../controller/functions/country_logic.dart';
class OsmMapView extends StatelessWidget { class OsmMapView extends StatelessWidget {
const OsmMapView({super.key}); const OsmMapView({super.key});
@@ -22,27 +23,77 @@ class OsmMapView extends StatelessWidget {
locationController.myLocation.longitude); locationController.myLocation.longitude);
final double currentHeading = locationController.heading; final double currentHeading = locationController.heading;
return IntaleqMap( final cityPolygon = CountryLogic.getCurrentCityPolygon();
apiKey: AK.mapSaasKey, final bool inServiceArea = CountryLogic.isPointInPolygon(currentLocation, cityPolygon);
initialCameraPosition: CameraPosition(
target: currentLocation, return Stack(
zoom: 15, children: [
bearing: currentHeading, IntaleqMap(
), apiKey: AK.mapSaasKey,
markers: { initialCameraPosition: CameraPosition(
Marker( target: currentLocation,
markerId: const MarkerId('myLocation'), zoom: 15,
position: currentLocation, bearing: currentHeading,
rotation: currentHeading, ),
icon: homeCaptainController.carIcon, markers: {
anchor: const Offset(0.5, 0.5), Marker(
flat: true, markerId: const MarkerId('myLocation'),
zIndex: 2, position: currentLocation,
) rotation: currentHeading,
}, icon: homeCaptainController.carIcon,
onMapCreated: (IntaleqMapController controller) { anchor: const Offset(0.5, 0.5),
// You can assign this controller if needed 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,
),
),
),
],
),
),
),
],
); );
}); });
} }

View File

@@ -398,6 +398,7 @@ static String get updateTesterApp => "$auth/Tester/updateTesterApp.php";
static String get signUp => "$auth/signup.php"; static String get signUp => "$auth/signup.php";
static String get sendVerifyEmail => "$auth/sendVerifyEmail.php"; static String get sendVerifyEmail => "$auth/sendVerifyEmail.php";
static String get loginFromGooglePassenger => "$auth/loginFromGooglePassenger.php"; static String get loginFromGooglePassenger => "$auth/loginFromGooglePassenger.php";
static String get loginUsingCredentialsWithoutGooglePassenger => "$auth/loginUsingCredentialsWithoutGooglePassenger.php";
static String get checkPhoneNumberISVerfiedPassenger => static String get checkPhoneNumberISVerfiedPassenger =>
"$auth/checkPhoneNumberISVerfiedPassenger.php"; "$auth/checkPhoneNumberISVerfiedPassenger.php";

View File

@@ -25,6 +25,7 @@ import 'package:location/location.dart';
import '../../env/env.dart'; import '../../env/env.dart';
import '../../print.dart'; import '../../print.dart';
import '../../views/auth/otp_token_page.dart'; import '../../views/auth/otp_token_page.dart';
import '../functions/country_logic.dart';
import '../functions/encrypt_decrypt.dart'; import '../functions/encrypt_decrypt.dart';
import '../functions/package_info.dart'; import '../functions/package_info.dart';
@@ -48,7 +49,8 @@ class LoginController extends GetxController {
@override @override
void onInit() async { void onInit() async {
// Log.print('box.read(BoxName.isTest): ${box.read(BoxName.isTest)}'); // Log.print('box.read(BoxName.isTest): ${box.read(BoxName.isTest)}');
box.write(BoxName.countryCode, 'Syria'); // كشف الدولة تلقائياً عبر الموقع الجغرافي
await CountryLogic.initializeCountry();
FirebaseMessagesController().getToken(); FirebaseMessagesController().getToken();
super.onInit(); super.onInit();
} }
@@ -271,8 +273,6 @@ class LoginController extends GetxController {
final res = await CRUD().get( final res = await CRUD().get(
link: AppLink.loginFromGooglePassenger, link: AppLink.loginFromGooglePassenger,
payload: { payload: {
'email': email,
'id': passengerID, // استخدم المعامل مباشرة
'platform': Platform.isAndroid ? 'android' : 'ios', 'platform': Platform.isAndroid ? 'android' : 'ios',
'appName': AppInformation.appName, 'appName': AppInformation.appName,
}, },
@@ -385,7 +385,7 @@ class LoginController extends GetxController {
"inviteCode": invite, "inviteCode": invite,
"passengerID": passengerID, "passengerID": passengerID,
}); });
// سجل الدعوة أيضاً في النظام الموحد الجديد // سجل الدعوة أيضاً في النظام الموحد الجديد
await CRUD().post(link: AppLink.addUnifiedInvite, payload: { await CRUD().post(link: AppLink.addUnifiedInvite, payload: {
"inviter_code": invite, "inviter_code": invite,
@@ -445,46 +445,48 @@ class LoginController extends GetxController {
// ); // );
// } // }
void login() async { void loginTester() async {
isloading = true; isloading = true;
update(); update();
var res =
await CRUD().get(link: AppLink.loginFromGooglePassenger, payload: { try {
'email': (emailController.text), var fingerPrint = await DeviceHelper.getDeviceFingerprint();
'id': passwordController.text, var payload = {
"platform": Platform.isAndroid ? 'android' : 'ios', 'email': emailController.text.trim(),
"appName": AppInformation.appName, '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.isVerified, '1');
box.write(BoxName.email, jsonDecoeded['data'][0]['email']); box.write(BoxName.email, d['email']);
box.write(BoxName.name, jsonDecoeded['data'][0]['first_name']); box.write(BoxName.name, d['first_name']);
box.write(BoxName.phone, jsonDecoeded['data'][0]['phone']); box.write(BoxName.phone, d['phone']);
box.write(BoxName.passengerID, passwordController.text); box.write(BoxName.passengerID, d['id']);
// var token = await CRUD().get(link: AppLink.getTokens, payload: { box.write(BoxName.isTest, '1');
// 'passengerID': box.read(BoxName.passengerID).toString()
// }); await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
// await updateAppTester(AppInformation.appName);
Get.offAll(() => const MapPagePassenger()); Get.offAll(() => const MapPagePassenger());
} else { } else {
// Get.offAll(() => SmsSignupEgypt()); Get.offAll(() => LoginPage());
// Get.snackbar(jsonDecoeded['status'], jsonDecoeded['data'],
// backgroundColor: Colors.redAccent);
isloading = false; isloading = false;
update(); update();
} }
@@ -492,6 +494,10 @@ class LoginController extends GetxController {
isloading = false; isloading = false;
update(); update();
} }
} catch (e) {
Log.print("Tester Login Error: $e");
isloading = false;
update();
} }
} }

View File

@@ -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/box_name.dart';
import 'package:siro_rider/constant/country_polygons.dart';
import 'package:siro_rider/main.dart'; import 'package:siro_rider/main.dart';
import '../../print.dart';
class CountryLogic { class CountryLogic {
/// Formats the phone number based on the country's dialing rules. /// Formats the phone number based on the country's dialing rules.
@@ -88,7 +92,77 @@ class CountryLogic {
return formatPhone(cleanPhone, 'Jordan'); return formatPhone(cleanPhone, 'Jordan');
} }
final country = box.read(BoxName.countryCode) ?? 'Syria'; final country = box.read(BoxName.countryCode) ?? 'Jordan';
return formatPhone(cleanPhone, country); 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');
}
}
} }

View File

@@ -75,14 +75,39 @@ class CRUD {
} }
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// دالة مساعدة خاصة: يجيب البصمة المشفرة من GetStorage // دالة مساعدة خاصة: تجيب البصمة المشفرة من GetStorage
// هي نفس القيمة المرسلة في login وعُملها hash في JWT payload
// السيرفر يعمل: sha256(X-Device-FP + FP_PEPPER) == JWT.fingerPrint
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
String _getFpHeader() { String _getFpHeader() {
return box.read(BoxName.deviceFpEncrypted)?.toString() ?? ''; 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. /// Centralized private method to handle all API requests.
/// Includes retry logic, network checking, and standardized error handling. /// Includes retry logic, network checking, and standardized error handling.
Future<dynamic> _makeRequest({ Future<dynamic> _makeRequest({
@@ -169,7 +194,7 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; final token = await _getJwt();
final headers = { final headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -193,14 +218,14 @@ class CRUD {
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,
}) async { }) async {
final token = await _getJwt();
var url = Uri.parse(link); var url = Uri.parse(link);
var response = await http.post( var response = await http.post(
url, url,
body: payload, body: payload,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Authorization': 'Bearer $token',
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز 'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
}, },
); );
@@ -548,7 +573,6 @@ class CRUD {
return response.statusCode; return response.statusCode;
} }
Future<dynamic> postPayMob({ Future<dynamic> postPayMob({
required String link, required String link,
Map<String, dynamic>? payload, Map<String, dynamic>? payload,

View File

@@ -4,12 +4,9 @@ import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/info.dart'; import 'package:siro_rider/constant/info.dart';
import 'package:siro_rider/controller/auth/login_controller.dart'; import 'package:siro_rider/controller/auth/login_controller.dart';
import 'package:jwt_decoder/jwt_decoder.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 '../../constant/links.dart';
import '../../main.dart'; import '../../main.dart';
import '../../print.dart';
import 'crud.dart'; import 'crud.dart';
import 'encrypt_decrypt.dart'; import 'encrypt_decrypt.dart';
@@ -24,23 +21,25 @@ class SecureStorage {
} }
} }
const List<String> keysToFetch = [
'serverPHP',
// 'seferAlexandriaServer',
// 'seferPaymentServer',
// 'seferCairoServer',
// 'seferGizaServer',
];
class AppInitializer { class AppInitializer {
List<Map<String, dynamic>> links = []; 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 { Future<void> initializeApp() async {
if (box.read(BoxName.jwt) == null) { final jwt = await _readJwt();
if (jwt == null) {
await LoginController().getJWT(); await LoginController().getJWT();
} else { } else {
bool isTokenExpired = JwtDecoder.isExpired( bool isTokenExpired =
r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0]); JwtDecoder.isExpired(r(jwt).toString().split(AppInformation.addd)[0]);
if (isTokenExpired) { if (isTokenExpired) {
await LoginController().getJWT(); await LoginController().getJWT();

View File

@@ -419,7 +419,7 @@ class LoginPage extends StatelessWidget {
onPressed: () { onPressed: () {
if (controller.formKey.currentState! if (controller.formKey.currentState!
.validate()) { .validate()) {
controller.login(); controller.loginTester();
} }
}, },
), ),

View File

@@ -160,7 +160,7 @@ class AuthScreen extends StatelessWidget {
testerEmailController.text; testerEmailController.text;
controller.passwordController.text = controller.passwordController.text =
testerPasswordController.text; testerPasswordController.text;
controller.login(); controller.loginTester();
Navigator.of(dialogContext).pop(); Navigator.of(dialogContext).pop();
} }
}, },