Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16b4829a6d | ||
|
|
a31242f947 | ||
|
|
cc9c5885b3 | ||
|
|
c5f8c6cd8e | ||
|
|
cbaf638abf | ||
|
|
940b69a554 | ||
|
|
53158598e7 | ||
|
|
86b5666595 | ||
|
|
b30fe7e96d |
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
// backend/bot/seed_kazan.php
|
||||||
|
// Populates the `kazan` table with default pricing for Jordan, Syria, and Egypt if missing
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../functions.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$con = Database::get('main');
|
||||||
|
echo "Connected to database.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("Database connection failed: " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultPricing = [
|
||||||
|
[
|
||||||
|
'country' => 'Jordan',
|
||||||
|
'currency' => 'JOD',
|
||||||
|
'kazanPercent' => '10',
|
||||||
|
'fuelPrice' => '1.0',
|
||||||
|
'startPrice' => '0.35',
|
||||||
|
'speedPrice' => '0.22',
|
||||||
|
'comfortPrice' => '0.29',
|
||||||
|
'ladyPrice' => '0.25',
|
||||||
|
'electricPrice' => '0.26',
|
||||||
|
'vanPrice' => '0.33',
|
||||||
|
'deliveryPrice' => '0.20',
|
||||||
|
'mishwarVipPrice' => '0.31',
|
||||||
|
'fixedPrice' => '0.22',
|
||||||
|
'awfarPrice' => '0.19',
|
||||||
|
'normalMinPrice' => '0.05',
|
||||||
|
'peakMinPrice' => '0.06',
|
||||||
|
'lateMinPrice' => '0.05',
|
||||||
|
'adminId' => 'system'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'country' => 'JO',
|
||||||
|
'currency' => 'JOD',
|
||||||
|
'kazanPercent' => '10',
|
||||||
|
'fuelPrice' => '1.0',
|
||||||
|
'startPrice' => '0.35',
|
||||||
|
'speedPrice' => '0.22',
|
||||||
|
'comfortPrice' => '0.29',
|
||||||
|
'ladyPrice' => '0.25',
|
||||||
|
'electricPrice' => '0.26',
|
||||||
|
'vanPrice' => '0.33',
|
||||||
|
'deliveryPrice' => '0.20',
|
||||||
|
'mishwarVipPrice' => '0.31',
|
||||||
|
'fixedPrice' => '0.22',
|
||||||
|
'awfarPrice' => '0.19',
|
||||||
|
'normalMinPrice' => '0.05',
|
||||||
|
'peakMinPrice' => '0.06',
|
||||||
|
'lateMinPrice' => '0.05',
|
||||||
|
'adminId' => 'system'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'country' => 'Syria',
|
||||||
|
'currency' => 'SYP',
|
||||||
|
'kazanPercent' => '10',
|
||||||
|
'fuelPrice' => '12000',
|
||||||
|
'startPrice' => '1000',
|
||||||
|
'speedPrice' => '500',
|
||||||
|
'comfortPrice' => '650',
|
||||||
|
'ladyPrice' => '550',
|
||||||
|
'electricPrice' => '600',
|
||||||
|
'vanPrice' => '750',
|
||||||
|
'deliveryPrice' => '450',
|
||||||
|
'mishwarVipPrice' => '700',
|
||||||
|
'fixedPrice' => '500',
|
||||||
|
'awfarPrice' => '425',
|
||||||
|
'normalMinPrice' => '100',
|
||||||
|
'peakMinPrice' => '115',
|
||||||
|
'lateMinPrice' => '100',
|
||||||
|
'adminId' => 'system'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'country' => 'Egypt',
|
||||||
|
'currency' => 'EGP',
|
||||||
|
'kazanPercent' => '10',
|
||||||
|
'fuelPrice' => '15',
|
||||||
|
'startPrice' => '10',
|
||||||
|
'speedPrice' => '3.0',
|
||||||
|
'comfortPrice' => '4.0',
|
||||||
|
'ladyPrice' => '3.3',
|
||||||
|
'electricPrice' => '3.6',
|
||||||
|
'vanPrice' => '4.5',
|
||||||
|
'deliveryPrice' => '2.7',
|
||||||
|
'mishwarVipPrice' => '4.2',
|
||||||
|
'fixedPrice' => '3.0',
|
||||||
|
'awfarPrice' => '2.5',
|
||||||
|
'normalMinPrice' => '0.5',
|
||||||
|
'peakMinPrice' => '0.6',
|
||||||
|
'lateMinPrice' => '0.5',
|
||||||
|
'adminId' => 'system'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'country' => 'EG',
|
||||||
|
'currency' => 'EGP',
|
||||||
|
'kazanPercent' => '10',
|
||||||
|
'fuelPrice' => '15',
|
||||||
|
'startPrice' => '10',
|
||||||
|
'speedPrice' => '3.0',
|
||||||
|
'comfortPrice' => '4.0',
|
||||||
|
'ladyPrice' => '3.3',
|
||||||
|
'electricPrice' => '3.6',
|
||||||
|
'vanPrice' => '4.5',
|
||||||
|
'deliveryPrice' => '2.7',
|
||||||
|
'mishwarVipPrice' => '4.2',
|
||||||
|
'fixedPrice' => '3.0',
|
||||||
|
'awfarPrice' => '2.5',
|
||||||
|
'normalMinPrice' => '0.5',
|
||||||
|
'peakMinPrice' => '0.6',
|
||||||
|
'lateMinPrice' => '0.5',
|
||||||
|
'adminId' => 'system'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$sqlInsert = "INSERT INTO kazan (
|
||||||
|
country, currency, kazanPercent, fuelPrice, startPrice,
|
||||||
|
speedPrice, comfortPrice, ladyPrice, electricPrice, vanPrice,
|
||||||
|
deliveryPrice, mishwarVipPrice, fixedPrice, awfarPrice,
|
||||||
|
normalMinPrice, peakMinPrice, lateMinPrice, adminId
|
||||||
|
) VALUES (
|
||||||
|
:country, :currency, :kazanPercent, :fuelPrice, :startPrice,
|
||||||
|
:speedPrice, :comfortPrice, :ladyPrice, :electricPrice, :vanPrice,
|
||||||
|
:deliveryPrice, :mishwarVipPrice, :fixedPrice, :awfarPrice,
|
||||||
|
:normalMinPrice, :peakMinPrice, :lateMinPrice, :adminId
|
||||||
|
) ON DUPLICATE KEY UPDATE
|
||||||
|
currency = VALUES(currency),
|
||||||
|
startPrice = VALUES(startPrice),
|
||||||
|
speedPrice = VALUES(speedPrice),
|
||||||
|
comfortPrice = VALUES(comfortPrice),
|
||||||
|
ladyPrice = VALUES(ladyPrice),
|
||||||
|
electricPrice = VALUES(electricPrice),
|
||||||
|
vanPrice = VALUES(vanPrice),
|
||||||
|
deliveryPrice = VALUES(deliveryPrice),
|
||||||
|
mishwarVipPrice = VALUES(mishwarVipPrice),
|
||||||
|
fixedPrice = VALUES(fixedPrice),
|
||||||
|
awfarPrice = VALUES(awfarPrice),
|
||||||
|
normalMinPrice = VALUES(normalMinPrice),
|
||||||
|
peakMinPrice = VALUES(peakMinPrice),
|
||||||
|
lateMinPrice = VALUES(lateMinPrice)";
|
||||||
|
|
||||||
|
$stmt = $con->prepare($sqlInsert);
|
||||||
|
|
||||||
|
foreach ($defaultPricing as $row) {
|
||||||
|
try {
|
||||||
|
$stmt->execute($row);
|
||||||
|
echo "Successfully seeded/updated kazan pricing for: {$row['country']}\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error seeding kazan for {$row['country']}: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Kazan seeding completed.\n";
|
||||||
|
|
||||||
|
// Also ensure ride_chat_logs table exists in main DB and ride DB
|
||||||
|
$sqlCreateChat = "CREATE TABLE IF NOT EXISTS `ride_chat_logs` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`ride_id` int NOT NULL,
|
||||||
|
`sender_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`receiver_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`sender_type` enum('passenger','driver') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_ride_id` (`ride_id`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$con->exec($sqlCreateChat);
|
||||||
|
echo "Successfully created/verified `ride_chat_logs` table in main DB.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error creating `ride_chat_logs` in main DB: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$con_ride = Database::get('ride');
|
||||||
|
$con_ride->exec($sqlCreateChat);
|
||||||
|
echo "Successfully created/verified `ride_chat_logs` table in ride DB.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error creating `ride_chat_logs` in ride DB: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,11 @@ class Database
|
|||||||
$user = getenv($cfg['user']);
|
$user = getenv($cfg['user']);
|
||||||
$pass = getenv($cfg['pass']);
|
$pass = getenv($cfg['pass']);
|
||||||
|
|
||||||
|
// Fallback for missing ride table if intaleq-ridesDB is still configured
|
||||||
|
if ($name === 'ride' && $dbname === 'intaleq-ridesDB') {
|
||||||
|
$dbname = 'rideDB';
|
||||||
|
}
|
||||||
|
|
||||||
if (!$dbname || !$user) {
|
if (!$dbname || !$user) {
|
||||||
error_log("[FATAL] Database config missing for: $name (Check ENV keys: {$cfg['name']}, {$cfg['user']})");
|
error_log("[FATAL] Database config missing for: $name (Check ENV keys: {$cfg['name']}, {$cfg['user']})");
|
||||||
throw new RuntimeException("Database configuration error.");
|
throw new RuntimeException("Database configuration error.");
|
||||||
|
|||||||
@@ -53,10 +53,43 @@ try {
|
|||||||
$stmt->execute([':id' => $id]);
|
$stmt->execute([':id' => $id]);
|
||||||
$driver = $stmt->fetch();
|
$driver = $stmt->fetch();
|
||||||
|
|
||||||
if (!$driver || empty($driver['password'])) {
|
if (!$driver) {
|
||||||
unauthorizedDriver();
|
unauthorizedDriver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── مسار السائقين المسجلين عبر OTP (بدون password في DB) ──
|
||||||
|
if (empty($driver['password'])) {
|
||||||
|
// التحقق من هوية السائق عبر fingerprint المخزن في driverToken
|
||||||
|
if (empty($fingerprint)) {
|
||||||
|
securityLog("LoginDriver(OTP): no fingerprint sent", [
|
||||||
|
'driver_id' => $driver['id'],
|
||||||
|
]);
|
||||||
|
unauthorizedDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtFp = $con->prepare('SELECT fingerPrint FROM driverToken WHERE captain_id = :id LIMIT 1');
|
||||||
|
$stmtFp->execute([':id' => $driver['id']]);
|
||||||
|
$fpRow = $stmtFp->fetch();
|
||||||
|
|
||||||
|
if (!$fpRow || empty($fpRow['fingerPrint'])) {
|
||||||
|
securityLog("LoginDriver(OTP): no stored fingerprint found", [
|
||||||
|
'driver_id' => $driver['id'],
|
||||||
|
]);
|
||||||
|
unauthorizedDriver();
|
||||||
|
}
|
||||||
|
|
||||||
|
$fpPepper = getenv('FP_PEPPER') ?: '';
|
||||||
|
$expectedFp = !empty($fpPepper) ? hash('sha256', $fingerprint . $fpPepper) : $fingerprint;
|
||||||
|
|
||||||
|
if (!hash_equals($fpRow['fingerPrint'], $expectedFp)) {
|
||||||
|
securityLog("LoginDriver(OTP): fingerprint mismatch", [
|
||||||
|
'driver_id' => $driver['id'],
|
||||||
|
]);
|
||||||
|
unauthorizedDriver();
|
||||||
|
}
|
||||||
|
// ✅ تم التحقق عبر fingerprint — نتابع لتوليد JWT
|
||||||
|
} else {
|
||||||
|
// ── المسار التقليدي: التحقق عبر password + HMAC ──────────
|
||||||
$decPhone = !empty($driver['phone']) ? $encryptionHelper->decryptData($driver['phone']) : null;
|
$decPhone = !empty($driver['phone']) ? $encryptionHelper->decryptData($driver['phone']) : null;
|
||||||
$decNat = !empty($driver['national_number']) ? $encryptionHelper->decryptData($driver['national_number']) : null;
|
$decNat = !empty($driver['national_number']) ? $encryptionHelper->decryptData($driver['national_number']) : null;
|
||||||
|
|
||||||
@@ -96,6 +129,7 @@ try {
|
|||||||
unauthorizedDriver();
|
unauthorizedDriver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$limiter->reset(RateLimiter::identifier(), 'login');
|
$limiter->reset(RateLimiter::identifier(), 'login');
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ function resolveServiceAccountPath(): string {
|
|||||||
if (!empty($envPath) && file_exists($envPath)) {
|
if (!empty($envPath) && file_exists($envPath)) {
|
||||||
return $envPath;
|
return $envPath;
|
||||||
}
|
}
|
||||||
// if (file_exists('/keys/firebase_service_account.json')) {
|
if (file_exists('/keys/firebase_service_account.json')) {
|
||||||
// return '/keys/firebase_service_account.json';
|
return '/keys/firebase_service_account.json';
|
||||||
// }
|
}
|
||||||
if (file_exists('/keys/service-account.json')) {
|
if (file_exists('/keys/service-account.json')) {
|
||||||
return '/keys/service-account.json';
|
return '/keys/service-account.json';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,12 +53,18 @@ try {
|
|||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
error_log("[get.php] api_get_nearby HTTP $httpCode | Response: $response");
|
||||||
|
|
||||||
$redisDrivers = [];
|
$redisDrivers = [];
|
||||||
if ($httpCode == 200 && $response) {
|
if ($httpCode == 200 && $response) {
|
||||||
$json = json_decode($response, true);
|
$json = json_decode($response, true);
|
||||||
if (isset($json['status']) && $json['status'] === true) {
|
if (isset($json['status']) && $json['status'] === true) {
|
||||||
$redisDrivers = $json['drivers'] ?? $json['data'] ?? [];
|
$redisDrivers = $json['drivers'] ?? $json['data'] ?? [];
|
||||||
|
} else {
|
||||||
|
error_log("[get.php] api_get_nearby returned error or false status. JSON: " . print_r($json, true));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
error_log("[get.php] api_get_nearby failed. HTTP Code: $httpCode");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($redisDrivers)) {
|
if (empty($redisDrivers)) {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ try {
|
|||||||
try {
|
try {
|
||||||
// Fetch ride data from remote/local DB for server-side calculation
|
// Fetch ride data from remote/local DB for server-side calculation
|
||||||
$stmtRideData = $con->prepare("
|
$stmtRideData = $con->prepare("
|
||||||
SELECT id, price AS quoted_price, car_type,
|
SELECT id, price AS quoted_price, carType,
|
||||||
distance AS planned_distance, passenger_id, driver_id, price_for_driver
|
distance AS planned_distance, passenger_id, driver_id, price_for_driver
|
||||||
FROM ride WHERE id = ? AND driver_id = ?
|
FROM ride WHERE id = ? AND driver_id = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -115,7 +115,7 @@ try {
|
|||||||
|
|
||||||
$quotedPrice = floatval($rideData['quoted_price'] ?? 0);
|
$quotedPrice = floatval($rideData['quoted_price'] ?? 0);
|
||||||
$kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد)
|
$kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد)
|
||||||
$carType = $rideData['car_type'] ?? 'Fixed Price';
|
$carType = $rideData['carType'] ?? 'Fixed Price';
|
||||||
|
|
||||||
// 🔥 [Fix Driver Commission] عند عرض السعر (pricing/get.php) قد تُطبَّق نسبة
|
// 🔥 [Fix Driver Commission] عند عرض السعر (pricing/get.php) قد تُطبَّق نسبة
|
||||||
// خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts)
|
// خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ try {
|
|||||||
status,
|
status,
|
||||||
start_location,
|
start_location,
|
||||||
end_location,
|
end_location,
|
||||||
car_type AS carType,
|
carType,
|
||||||
driver_id,distance,
|
driver_id,distance,
|
||||||
price,
|
price,
|
||||||
created_at
|
created_at
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ try {
|
|||||||
|
|
||||||
// 🆕 Immutable Fare Lock: Save agreed fixed price in Redis
|
// 🆕 Immutable Fare Lock: Save agreed fixed price in Redis
|
||||||
try {
|
try {
|
||||||
$stmtRideInfo = $con->prepare("SELECT price, car_type FROM ride WHERE id = ? LIMIT 1");
|
$stmtRideInfo = $con->prepare("SELECT price, carType FROM ride WHERE id = ? LIMIT 1");
|
||||||
$stmtRideInfo->execute([$ride_id]);
|
$stmtRideInfo->execute([$ride_id]);
|
||||||
$rideInfo = $stmtRideInfo->fetch(PDO::FETCH_ASSOC);
|
$rideInfo = $stmtRideInfo->fetch(PDO::FETCH_ASSOC);
|
||||||
if ($rideInfo) {
|
if ($rideInfo) {
|
||||||
$agreedPrice = floatval($rideInfo['price']);
|
$agreedPrice = floatval($rideInfo['price']);
|
||||||
$carTypeStr = $rideInfo['car_type'] ?? 'Fixed Price';
|
$carTypeStr = $rideInfo['carType'] ?? 'Fixed Price';
|
||||||
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
|
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
|
||||||
if (in_array($carTypeStr, $fixedPriceTypes)) {
|
if (in_array($carTypeStr, $fixedPriceTypes)) {
|
||||||
global $redis;
|
global $redis;
|
||||||
|
|||||||
@@ -2188,3 +2188,18 @@ CREATE TABLE IF NOT EXISTS `competitor_surge_insights` (
|
|||||||
`detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
`detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`)
|
UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 4. Chat Logs Table
|
||||||
|
CREATE TABLE IF NOT EXISTS `ride_chat_logs` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`ride_id` int NOT NULL,
|
||||||
|
`sender_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`receiver_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`sender_type` enum('passenger','driver') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_ride_id` (`ride_id`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
|||||||
@@ -1696,6 +1696,25 @@ CREATE TABLE `write_argument_after_applied_from_background` (
|
|||||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `ride_chat_logs`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `ride_chat_logs`;
|
||||||
|
CREATE TABLE IF NOT EXISTS `ride_chat_logs` (
|
||||||
|
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`ride_id` int NOT NULL,
|
||||||
|
`sender_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`receiver_id` varchar(111) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`sender_type` enum('passenger','driver') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`message_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_ride_id` (`ride_id`),
|
||||||
|
KEY `idx_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Dumping events for database 'intaleq-ridesDB'
|
-- Dumping events for database 'intaleq-ridesDB'
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# إنهاء TLS أمام سوكيتات Workerman — تطبيق intaleq
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
# يُنسَخ على **المضيف** لا داخل الحاويات:
|
||||||
|
# /etc/nginx/sites-enabled/intaleq-sockets-tls.conf
|
||||||
|
#
|
||||||
|
# نفس منطق siro-sockets-tls.conf تماماً، لكن هذا التطبيق (intaleq) يطلب
|
||||||
|
# https://api.intaleqapp.com:2020 و :3030 — دومين مختلف عن دومين سيرو
|
||||||
|
# (jordan-siro.intaleqapp.com). بما أن كلا التطبيقين على نفس السيرفر
|
||||||
|
# ويستخدمان نفس المنفذين الخارجيين 2020/3030، فالفصل بينهما يتم عبر
|
||||||
|
# SNI: كل server{} هنا يطابق دومينه فقط، ونجينكس يختار الحاوية الصحيحة
|
||||||
|
# من اسم الدومين في مصافحة TLS — بلا أي حاجة لتغيير المنفذين الخارجيين.
|
||||||
|
#
|
||||||
|
# الحاويتان تُنشران على 127.0.0.1 فقط (12032 / 13032 — انظر
|
||||||
|
# docker-compose.yml، DRIVER_SOCKET_PORT / PASSENGER_SOCKET_PORT)، وهذان
|
||||||
|
# المنفذان مختلفان فعلاً عن منفذي سيرو (12020 / 13030) فلا تصادم بينهما.
|
||||||
|
#
|
||||||
|
# تحقّق أولاً من مسار شهادة api.intaleqapp.com الصحيح:
|
||||||
|
# grep -rh ssl_certificate /etc/nginx/sites-enabled/ | grep -v key | sort -u
|
||||||
|
# ══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
map $http_upgrade $intaleq_connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── سوكيت السائقين — GPS ─────────────────────────────────────────
|
||||||
|
server {
|
||||||
|
listen 2020 ssl;
|
||||||
|
listen [::]:2020 ssl;
|
||||||
|
|
||||||
|
server_name api.intaleqapp.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl-certificates/api.intaleqapp.com.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl-certificates/api.intaleqapp.com.key;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:IntaleqSock:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log /var/log/nginx/intaleq-socket-driver-error.log warn;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:12032;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $intaleq_connection_upgrade;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── سوكيت الركاب — حالة الرحلة وموقع السائق ──────────────────────
|
||||||
|
server {
|
||||||
|
listen 3030 ssl;
|
||||||
|
listen [::]:3030 ssl;
|
||||||
|
|
||||||
|
server_name api.intaleqapp.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl-certificates/api.intaleqapp.com.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl-certificates/api.intaleqapp.com.key;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_session_cache shared:IntaleqSock:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log /var/log/nginx/intaleq-socket-passenger-error.log warn;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:13032;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $intaleq_connection_upgrade;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,5 +4,5 @@
|
|||||||
تشغيل التطبيق على هذا الجهاز.</string>
|
تشغيل التطبيق على هذا الجهاز.</string>
|
||||||
<string name="exit_button">إغلاق التطبيق</string>
|
<string name="exit_button">إغلاق التطبيق</string>
|
||||||
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
||||||
<string name="label">سيرو درايفر</string>
|
<string name="label">انطلق كابتن</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1 +1 @@
|
|||||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:android:492d9bc1df6b40c51632ca","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:a86f1a54f0b1aaa21632ca","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"siro-a6957","configurations":{"android":"1:825988584191:android:492d9bc1df6b40c51632ca","ios":"1:825988584191:ios:a86f1a54f0b1aaa21632ca"}}}}}}
|
{"flutter":{"platforms":{"android":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:android:e3daebe53bf691de77a35f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:ios:6d2c7f479c82ba7077a35f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"intaleq-d48a7","configurations":{"android":"1:1086900987150:android:e3daebe53bf691de77a35f","ios":"1:1086900987150:ios:6d2c7f479c82ba7077a35f"}}}}}}
|
||||||
@@ -512,6 +512,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -522,7 +523,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver;
|
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -537,6 +538,7 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
|
||||||
@@ -555,6 +557,7 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
|
||||||
@@ -571,6 +574,7 @@
|
|||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver.RunnerTests;
|
||||||
@@ -698,6 +702,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -708,7 +713,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver;
|
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -724,6 +729,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -734,7 +740,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Intaleq.driver;
|
PRODUCT_BUNDLE_IDENTIFIER = "com.intaleq-driver";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 616 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 968 B |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -84,32 +84,25 @@
|
|||||||
<string>_dartobservatory._tcp</string>
|
<string>_dartobservatory._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>This app requires access to your camera in order to scan QR codes and capture images
|
<string>Intaleq Driver uses your camera to capture required registration documents (such as driver's license and vehicle registration) and take a profile photo.</string>
|
||||||
for uploading and access to connect to a call.</string>
|
|
||||||
<key>NSContactsUsageDescription</key>
|
<key>NSContactsUsageDescription</key>
|
||||||
<string>This app requires contacts access to function properly.</string>
|
<string>Intaleq Driver accesses your contacts so you can quickly select emergency contact details and share trip status updates with trusted contacts.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Use Face ID to securely authenticate payment accounts.</string>
|
<string>Intaleq Driver uses Face ID to securely authenticate driver sign-in and authorize earnings payout requests.</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>Allow debugging over the local network.</string>
|
<string>Intaleq Driver uses local network access for local testing and debugging network connections during development.</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>This app needs access to your location to provide you with the best ride experience.
|
<string>Intaleq Driver continuously accesses your location to receive nearby trip requests, navigate to pickup and drop-off locations, calculate trip fares, and share real-time position with passengers even when the app is running in the background.</string>
|
||||||
Your location data will be used to find the nearest available cars and connect you with
|
|
||||||
the closest captain for efficient and convenient rides.</string>
|
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
<string>This app needs access to location.</string>
|
<string>Intaleq Driver requires background location access to keep your vehicle status active, route you to rider locations, and track completed mileage even when the app is minimized.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>This app needs access to your location to provide you with the best ride experience.
|
<string>Intaleq Driver uses your location while using the app to present available pickup requests nearby and provide turn-by-turn navigation during active trips.</string>
|
||||||
Your location data will be used to find the nearest available cars and connect you with
|
|
||||||
the closest captain for efficient and convenient rides.</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>This app requires access to your microphone to record audio, allowing you to add
|
<string>Intaleq Driver uses your microphone to enable in-app voice calls with passengers for pickup coordination and safety communication.</string>
|
||||||
voice recordings to your photos and videos and access to connect to a call.</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>This app needs access to your photo library to allow you to upload and manage
|
<string>Intaleq Driver accesses your photo library to allow uploading vehicle registration documents, driver identification cards, and profile pictures from your photo gallery.</string>
|
||||||
images.</string>
|
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ class LoginDriverController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dev = '';
|
var dev = '';
|
||||||
Future<String>? _walletJwtFuture;
|
// static: لأن CRUD ينشئ LoginDriverController() جديد بكل طلب،
|
||||||
|
// فلو كانت instance field ما بيشتغل الـ single-flight أبداً.
|
||||||
|
static Future<String>? _walletJwtFuture;
|
||||||
|
|
||||||
getJwtWallet() async {
|
getJwtWallet() async {
|
||||||
if (_walletJwtFuture != null) {
|
if (_walletJwtFuture != null) {
|
||||||
@@ -228,7 +230,41 @@ class LoginDriverController extends GetxController {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getJWT() async {
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// getJWT — تجديد توكن السائق
|
||||||
|
// • single-flight: طلب تجديد واحد فقط بنفس اللحظة (static)
|
||||||
|
// • بيرجّع true/false حتى المستدعي يعرف نجح ولا لأ
|
||||||
|
// • cooldown بعد الفشل: يمنع ضرب /loginJwtDriver.php (حده 5/دقيقة)
|
||||||
|
// بحلقة لا نهائية تنتهي بـ 429 "Please slow down"
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
static Future<bool>? _jwtFuture;
|
||||||
|
static DateTime _jwtCooldownUntil = DateTime(2000);
|
||||||
|
static int _jwtFailures = 0;
|
||||||
|
|
||||||
|
/// الوقت المتبقّي على انتهاء الـ cooldown (0 يعني مسموح التجديد)
|
||||||
|
static Duration get jwtCooldownRemaining {
|
||||||
|
final d = _jwtCooldownUntil.difference(DateTime.now());
|
||||||
|
return d.isNegative ? Duration.zero : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getJWT() async {
|
||||||
|
if (_jwtFuture != null) {
|
||||||
|
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
|
||||||
|
return _jwtFuture!;
|
||||||
|
}
|
||||||
|
_jwtFuture = _getJwtInternal().catchError((e) {
|
||||||
|
// أي استثناء (timeout/socket) لازم يتحوّل لـ false + cooldown،
|
||||||
|
// مش يطلع للمستدعي ويكسر الطلب اللي فوق
|
||||||
|
return _onJwtFailure('exception: $e');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return await _jwtFuture!;
|
||||||
|
} finally {
|
||||||
|
_jwtFuture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _getJwtInternal() async {
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
// 1. Check secure storage first to avoid redundant API calls
|
// 1. Check secure storage first to avoid redundant API calls
|
||||||
@@ -259,14 +295,31 @@ class LoginDriverController extends GetxController {
|
|||||||
|
|
||||||
if (isTokenValid) {
|
if (isTokenValid) {
|
||||||
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
|
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
|
||||||
return;
|
_jwtFailures = 0;
|
||||||
|
_jwtCooldownUntil = DateTime(2000);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// التوكن منتهي فعلاً — بس لو آخر محاولة فشلت لازم ننتظر قبل ما نعيد
|
||||||
|
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
|
||||||
|
Log.print(
|
||||||
|
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||||
Log.print(
|
Log.print(
|
||||||
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
|
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
|
||||||
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
// ⚠️ loginFirstTimeDriver.php بيرجّع توكن نوعه "registration" (صالح ساعة
|
||||||
|
// وone-time)، وباقي الـ endpoints بترفضه بـ 401. فما بنستخدمه إلا لما ما
|
||||||
|
// يكون عنا driverID أصلاً (سائق لسا ما تسجّل). أي سائق عنده ID → تجديد عادي
|
||||||
|
// عبر loginJwtDriver.php حتى لو firstTimeLoadKey ما انكتب.
|
||||||
|
final driverId = box.read(BoxName.driverID);
|
||||||
|
final isRegistering =
|
||||||
|
driverId == null || driverId.toString().isEmpty;
|
||||||
|
|
||||||
|
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.driverID) ?? AK.newId,
|
'id': box.read(BoxName.driverID) ?? AK.newId,
|
||||||
'password': AK.passnpassenger,
|
'password': AK.passnpassenger,
|
||||||
@@ -276,12 +329,21 @@ class LoginDriverController extends GetxController {
|
|||||||
};
|
};
|
||||||
// Log.print('payload: ${payload}');
|
// Log.print('payload: ${payload}');
|
||||||
|
|
||||||
var response0 = await http.post(
|
var response0 = await http
|
||||||
|
.post(
|
||||||
Uri.parse(AppLink.loginFirstTimeDriver),
|
Uri.parse(AppLink.loginFirstTimeDriver),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
Log.print('response0: ${response0.body}');
|
Log.print('response0: ${response0.body}');
|
||||||
Log.print('request: ${response0.request}');
|
Log.print('request: ${response0.request}');
|
||||||
|
if (response0.statusCode == 429) {
|
||||||
|
final retryAfter =
|
||||||
|
int.tryParse(response0.headers['retry-after'] ?? '') ?? 60;
|
||||||
|
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
|
||||||
|
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (response0.statusCode == 200) {
|
if (response0.statusCode == 200) {
|
||||||
final decodedResponse1 = jsonDecode(response0.body);
|
final decodedResponse1 = jsonDecode(response0.body);
|
||||||
Log.print('decodedResponse1: ${decodedResponse1}');
|
Log.print('decodedResponse1: ${decodedResponse1}');
|
||||||
@@ -297,31 +359,46 @@ class LoginDriverController extends GetxController {
|
|||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
// box.write(BoxName.jwt, c(jwt));
|
// box.write(BoxName.jwt, c(jwt));
|
||||||
await storage.write(key: BoxName.jwt, value: jwt);
|
await storage.write(key: BoxName.jwt, value: jwt);
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
return _onJwtSuccess();
|
||||||
// await AppInitializer().getKey();
|
}
|
||||||
} else {}
|
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
|
||||||
|
}
|
||||||
|
return _onJwtFailure('firstTime: HTTP ${response0.statusCode}');
|
||||||
} else {
|
} else {
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
|
// بدون driverID التجديد مضمون الفشل — نوقف بدون ما نضرب السيرفر
|
||||||
|
if (isRegistering) {
|
||||||
|
return _onJwtFailure('renew: لا يوجد driverID');
|
||||||
|
}
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.driverID),
|
'id': driverId,
|
||||||
'password': box.read(BoxName.emailDriver),
|
|
||||||
'aud': '${AK.allowed}$dev',
|
'aud': '${AK.allowed}$dev',
|
||||||
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
||||||
await DeviceHelper.getDeviceFingerprint(),
|
await DeviceHelper.getDeviceFingerprint(),
|
||||||
};
|
};
|
||||||
// print(payload);
|
// print(payload);
|
||||||
var response1 = await http.post(
|
var response1 = await http
|
||||||
|
.post(
|
||||||
Uri.parse(AppLink.loginJwtDriver),
|
Uri.parse(AppLink.loginJwtDriver),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
Log.print('response1.request: ${response1.request}');
|
Log.print('response1.request: ${response1.request}');
|
||||||
Log.print('response1.body: ${response1.body}');
|
Log.print('response1.body: ${response1.body}');
|
||||||
|
|
||||||
|
// 429 → السيرفر عامل rate limit؛ نحترم Retry-After ولا نعيد المحاولة
|
||||||
|
if (response1.statusCode == 429) {
|
||||||
|
final retryAfter =
|
||||||
|
int.tryParse(response1.headers['retry-after'] ?? '') ?? 60;
|
||||||
|
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
|
||||||
|
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (response1.statusCode == 200) {
|
if (response1.statusCode == 200) {
|
||||||
final decodedResponse1 = jsonDecode(response1.body);
|
final decodedResponse1 = jsonDecode(response1.body);
|
||||||
// Log.print('decodedResponse1: ${decodedResponse1}');
|
// Log.print('decodedResponse1: ${decodedResponse1}');
|
||||||
@@ -337,11 +414,30 @@ class LoginDriverController extends GetxController {
|
|||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
// await box.write(BoxName.jwt, c(jwt));
|
// await box.write(BoxName.jwt, c(jwt));
|
||||||
await storage.write(key: BoxName.jwt, value: jwt);
|
await storage.write(key: BoxName.jwt, value: jwt);
|
||||||
|
return _onJwtSuccess();
|
||||||
|
}
|
||||||
|
return _onJwtFailure('renew: لا يوجد jwt بالرد');
|
||||||
|
}
|
||||||
|
return _onJwtFailure('renew: HTTP ${response1.statusCode}');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// await AppInitializer().getKey();
|
// نجاح التجديد → تصفير العدّاد والـ cooldown
|
||||||
}
|
bool _onJwtSuccess() {
|
||||||
|
_jwtFailures = 0;
|
||||||
|
_jwtCooldownUntil = DateTime(2000);
|
||||||
|
Log.print('✅ getJWT: تم توليد توكن جديد.');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// فشل التجديد → backoff تصاعدي (2,4,8,16,32,60ث كحد أقصى)
|
||||||
|
// هذا هو اللي بيمنع الحلقة اللانهائية اللي بتوصل لـ rate limit
|
||||||
|
bool _onJwtFailure(String reason) {
|
||||||
|
_jwtFailures++;
|
||||||
|
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
|
||||||
|
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
|
||||||
|
Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getLocationPermission() async {
|
Future<void> getLocationPermission() async {
|
||||||
@@ -653,10 +749,8 @@ class LoginDriverController extends GetxController {
|
|||||||
var d = jsonDecoeded['data'][0];
|
var d = jsonDecoeded['data'][0];
|
||||||
var jwt = jsonDecoeded['jwt'];
|
var jwt = jsonDecoeded['jwt'];
|
||||||
|
|
||||||
// حفظ التوكن أولاً
|
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
box.write(BoxName.jwt, c(jwt));
|
await storage.write(key: BoxName.jwt, value: jwt.toString());
|
||||||
await storage.write(key: BoxName.jwt, value: c(jwt));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
box.write(BoxName.emailDriver, (d['email']));
|
box.write(BoxName.emailDriver, (d['email']));
|
||||||
|
|||||||
@@ -371,8 +371,7 @@ class RegistrationController extends GetxController {
|
|||||||
final uri = Uri.parse(currentUrl);
|
final uri = Uri.parse(currentUrl);
|
||||||
final request = http.MultipartRequest('POST', uri);
|
final request = http.MultipartRequest('POST', uri);
|
||||||
|
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final String _token = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
|
||||||
|
|
||||||
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
String nonce = timestamp;
|
String nonce = timestamp;
|
||||||
@@ -581,8 +580,8 @@ class RegistrationController extends GetxController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// ترويسات مشتركة
|
// ترويسات مشتركة
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final String _jwtToken = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final bearer = 'Bearer ${_jwt != null ? r(_jwt).split(AppInformation.addd)[0] : ''}';
|
final bearer = 'Bearer $_jwtToken';
|
||||||
final hmac = '${box.read(BoxName.hmac)}';
|
final hmac = '${box.read(BoxName.hmac)}';
|
||||||
|
|
||||||
String fingerPrint =
|
String fingerPrint =
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
|
|
||||||
case 'token change':
|
case 'token change':
|
||||||
case 'TOKEN_CHANGE':
|
case 'TOKEN_CHANGE':
|
||||||
box.remove(BoxName.jwt);
|
await storage.delete(key: BoxName.jwt);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'face detect':
|
case 'face detect':
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import 'ssl_pinning.dart';
|
|||||||
class CRUD {
|
class CRUD {
|
||||||
final NetGuard _netGuard = NetGuard();
|
final NetGuard _netGuard = NetGuard();
|
||||||
final _client = SslPinning.createPinnedClient();
|
final _client = SslPinning.createPinnedClient();
|
||||||
static bool _isRefreshingJWT = false;
|
|
||||||
static String _lastErrorSignature = '';
|
static String _lastErrorSignature = '';
|
||||||
static DateTime _lastErrorTimestamp = DateTime(2000);
|
static DateTime _lastErrorTimestamp = DateTime(2000);
|
||||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||||
@@ -107,6 +106,21 @@ class CRUD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
|
||||||
|
// • getJWT() نفسها single-flight + فيها cooldown بعد الفشل،
|
||||||
|
// فما في داعي لأي flag هون — ولا في خطر حلقة لا نهائية.
|
||||||
|
// • بترجع التوكن الصالح أو '' لو التجديد فشل/بـ cooldown.
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
Future<String> _ensureJwt() async {
|
||||||
|
String token = await _getJwt();
|
||||||
|
if (_isJwtValid(token)) return token;
|
||||||
|
|
||||||
|
final ok = await Get.put(LoginDriverController()).getJWT();
|
||||||
|
if (!ok) return '';
|
||||||
|
return await _getJwt();
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// _makeRequest — دالة مركزية لكل الطلبات
|
// _makeRequest — دالة مركزية لكل الطلبات
|
||||||
// ───────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────
|
||||||
@@ -119,6 +133,7 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
required Map<String, String> headers,
|
required Map<String, String> headers,
|
||||||
|
bool allowRefresh = true,
|
||||||
}) async {
|
}) async {
|
||||||
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
|
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
|
||||||
const totalTimeout = Duration(seconds: 60);
|
const totalTimeout = Duration(seconds: 60);
|
||||||
@@ -186,19 +201,33 @@ class CRUD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
|
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
|
||||||
|
if (sc == 429) {
|
||||||
|
Log.print('🛑 [RES-$requestId] 429 rate limited — $link');
|
||||||
|
return 'rate_limited';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
|
||||||
if (sc == 401) {
|
if (sc == 401) {
|
||||||
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
||||||
final isNonCritical = link.contains('errorApp.php');
|
final isNonCritical = link.contains('errorApp.php');
|
||||||
if (!_isRefreshingJWT && !isNonCritical) {
|
if (isNonCritical || !allowRefresh) return 'token_expired';
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
final refreshed = await Get.put(LoginDriverController()).getJWT();
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
if (!refreshed) return 'token_expired';
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
final newToken = await _getJwt();
|
||||||
}
|
if (newToken.isEmpty) return 'token_expired';
|
||||||
}
|
|
||||||
return 'token_expired';
|
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
|
||||||
|
final retryHeaders = Map<String, String>.from(headers)
|
||||||
|
..['Authorization'] = 'Bearer $newToken';
|
||||||
|
return await _makeRequest(
|
||||||
|
link: link,
|
||||||
|
payload: payload,
|
||||||
|
headers: retryHeaders,
|
||||||
|
allowRefresh: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5xx
|
// 5xx
|
||||||
@@ -218,18 +247,9 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
String token = await _getJwt();
|
|
||||||
|
|
||||||
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
|
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
|
||||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
final String token = await _ensureJwt();
|
||||||
_isRefreshingJWT = true;
|
if (token.isEmpty) return 'token_expired';
|
||||||
try {
|
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
|
||||||
token = await _getJwt();
|
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
@@ -250,16 +270,8 @@ class CRUD {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// فحص صلاحية التوكن قبل الإرسال
|
// فحص صلاحية التوكن قبل الإرسال
|
||||||
String token = await _getJwt();
|
final String token = await _ensureJwt();
|
||||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
if (token.isEmpty) return 'token_expired';
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
|
||||||
token = await _getJwt();
|
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = Uri.parse(link);
|
var url = Uri.parse(link);
|
||||||
var response = await _client.post(
|
var response = await _client.post(
|
||||||
@@ -279,15 +291,12 @@ class CRUD {
|
|||||||
var jsonData = jsonDecode(response.body);
|
var jsonData = jsonDecode(response.body);
|
||||||
if (jsonData['status'] == 'success') return response.body;
|
if (jsonData['status'] == 'success') return response.body;
|
||||||
return jsonData['status'];
|
return jsonData['status'];
|
||||||
|
} else if (response.statusCode == 429) {
|
||||||
|
Log.print('🛑 get: 429 rate limited — $link');
|
||||||
|
return 'rate_limited';
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
if (!_isRefreshingJWT) {
|
// تجديد واحد فقط؛ getJWT فيها single-flight + cooldown
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
await Get.put(LoginDriverController()).getJWT();
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'token_expired';
|
return 'token_expired';
|
||||||
} else if (response.statusCode >= 500) {
|
} else if (response.statusCode >= 500) {
|
||||||
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
||||||
@@ -572,12 +581,8 @@ class CRUD {
|
|||||||
// ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ─────────
|
// ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ─────────
|
||||||
Future<void> sendEmail(String link, Map<String, String>? payload) async {
|
Future<void> sendEmail(String link, Map<String, String>? payload) async {
|
||||||
// r() هي نفس دالة فك التشفير الثلاثي المختصرة
|
// r() هي نفس دالة فك التشفير الثلاثي المختصرة
|
||||||
String token = await _getJwt();
|
final String token = await _ensureJwt();
|
||||||
|
if (token.isEmpty) return;
|
||||||
if (!_isJwtValid(token)) {
|
|
||||||
await LoginDriverController().getJWT();
|
|
||||||
token = await _getJwt();
|
|
||||||
}
|
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
|||||||
@@ -479,8 +479,7 @@ class AI extends GetxController {
|
|||||||
bool isLoadingVehicleFrontSy = false;
|
bool isLoadingVehicleFrontSy = false;
|
||||||
bool isLoadingVehicleBackSy = false;
|
bool isLoadingVehicleBackSy = false;
|
||||||
Future<void> sendToAI(String type, {required File imageFile}) async {
|
Future<void> sendToAI(String type, {required File imageFile}) async {
|
||||||
final _jwt = box.read(BoxName.jwt);
|
final String _token = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final String _token = _jwt != null ? r(_jwt).split(AppInformation.addd)[0] : '';
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Authorization': 'Bearer $_token',
|
'Authorization': 'Bearer $_token',
|
||||||
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||||
|
|||||||
@@ -488,7 +488,7 @@ class ScanDocumentsByApi extends GetxController {
|
|||||||
update();
|
update();
|
||||||
|
|
||||||
final String token =
|
final String token =
|
||||||
box.read(BoxName.jwt)?.toString().split(AppInformation.addd)[0] ?? '';
|
await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final String fingerPrint =
|
final String fingerPrint =
|
||||||
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||||
final String driverID = box.read(BoxName.driverID).toString();
|
final String driverID = box.read(BoxName.driverID).toString();
|
||||||
|
|||||||
@@ -312,8 +312,7 @@ class ImageController extends GetxController {
|
|||||||
required String filename,
|
required String filename,
|
||||||
required String methodLabel,
|
required String methodLabel,
|
||||||
}) async {
|
}) async {
|
||||||
final jwt = box.read(BoxName.jwt);
|
final String token = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
final String token = jwt != null ? r(jwt).split(AppInformation.addd)[0] : '';
|
|
||||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||||
final int fileSizeBytes = await file.length();
|
final int fileSizeBytes = await file.length();
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,7 @@ class ComplaintController extends GetxController {
|
|||||||
|
|
||||||
var uri = Uri.parse(AppLink.uploadAudio);
|
var uri = Uri.parse(AppLink.uploadAudio);
|
||||||
var request = http.MultipartRequest('POST', uri);
|
var request = http.MultipartRequest('POST', uri);
|
||||||
final jwt = box.read(BoxName.jwt);
|
String token = await storage.read(key: BoxName.jwt) ?? '';
|
||||||
String token = jwt != null ? r(jwt).toString().split(Env.addd)[0] : '';
|
|
||||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||||
|
|
||||||
var mimeType = lookupMimeType(audioFile.path);
|
var mimeType = lookupMimeType(audioFile.path);
|
||||||
|
|||||||
@@ -50,18 +50,20 @@ class DefaultFirebaseOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const FirebaseOptions android = FirebaseOptions(
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyAEoply_UcEP6KaCu_ziCy_ZDIjAKvi7b8',
|
apiKey: 'AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64',
|
||||||
appId: '1:825988584191:android:492d9bc1df6b40c51632ca',
|
appId: '1:1086900987150:android:e3daebe53bf691de77a35f',
|
||||||
messagingSenderId: '825988584191',
|
messagingSenderId: '1086900987150',
|
||||||
projectId: 'siro-a6957',
|
projectId: 'intaleq-d48a7',
|
||||||
storageBucket: 'siro-a6957.firebasestorage.app',
|
storageBucket: 'intaleq-d48a7.firebasestorage.app',
|
||||||
);
|
);
|
||||||
static const FirebaseOptions ios = FirebaseOptions(
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
|
apiKey: 'AIzaSyAwG09AeehwBfktpKKJwCKQOtEUpHtr-p0',
|
||||||
appId: '1:825988584191:ios:a86f1a54f0b1aaa21632ca',
|
appId: '1:1086900987150:ios:6d2c7f479c82ba7077a35f',
|
||||||
messagingSenderId: '825988584191',
|
messagingSenderId: '1086900987150',
|
||||||
projectId: 'siro-a6957',
|
projectId: 'intaleq-d48a7',
|
||||||
storageBucket: 'siro-a6957.firebasestorage.app',
|
storageBucket: 'intaleq-d48a7.firebasestorage.app',
|
||||||
iosBundleId: 'com.siro.siro-driver',
|
androidClientId: '1086900987150-060srlmdjocdcav377rbur4ka14m90b7.apps.googleusercontent.com',
|
||||||
|
iosClientId: '1086900987150-6mpaq0ookehlvljtsp2aes26dqpukok1.apps.googleusercontent.com',
|
||||||
|
iosBundleId: 'com.Intaleq.driver',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ dev_dependencies:
|
|||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: "launcher_icon"
|
android: "launcher_icon"
|
||||||
ios: true
|
ios: true
|
||||||
|
remove_alpha_ios: true
|
||||||
image_path: "assets/images/logo.png"
|
image_path: "assets/images/logo.png"
|
||||||
min_sdk_android: 21
|
min_sdk_android: 21
|
||||||
web:
|
web:
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
تشغيل التطبيق على هذا الجهاز.</string>
|
تشغيل التطبيق على هذا الجهاز.</string>
|
||||||
<string name="exit_button">إغلاق التطبيق</string>
|
<string name="exit_button">إغلاق التطبيق</string>
|
||||||
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
||||||
<string name="label">سيرو</string>
|
<string name="label">انطلق</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1 +1 @@
|
|||||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:android:7e9088b719dcb7061632ca","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:4f60966dd0a69b3f1632ca","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"siro-a6957","configurations":{"android":"1:825988584191:android:7e9088b719dcb7061632ca","ios":"1:825988584191:ios:4f60966dd0a69b3f1632ca","macos":"1:825988584191:ios:7b48f585862d5cfc1632ca"}}},"macos":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:7b48f585862d5cfc1632ca","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}}}}}
|
{"flutter":{"platforms":{"android":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:android:b7231956aa6d3b3377a35f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"intaleq-d48a7","appId":"1:1086900987150:ios:a60973accd7f3dc777a35f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"intaleq-d48a7","configurations":{"android":"1:1086900987150:android:b7231956aa6d3b3377a35f","ios":"1:1086900987150:ios:a60973accd7f3dc777a35f"}}},"macos":{"default":{"projectId":"siro-a6957","appId":"1:825988584191:ios:7b48f585862d5cfc1632ca","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}}}}}
|
||||||
@@ -662,6 +662,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -851,6 +852,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -877,6 +879,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
DEVELOPMENT_TEAM = 63CVT8G5P8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 649 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
@@ -58,27 +58,21 @@
|
|||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>This app requires access to your camera in order to scan QR codes and capture images
|
<string>Intaleq Rider uses your camera so you can take a profile picture, scan QR codes for quick ride check-ins, or attach photos when reporting trip issues to support.</string>
|
||||||
for uploading and access to connect to a call.</string>
|
|
||||||
<key>NSContactsUsageDescription</key>
|
<key>NSContactsUsageDescription</key>
|
||||||
<string>This app requires contacts access to function properly.</string>
|
<string>Intaleq Rider accesses your contacts so you can easily select emergency contacts and share live trip status with family or friends.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Use Face ID to securely authenticate payment accounts.</string>
|
<string>Intaleq Rider uses Face ID to securely authenticate payment transactions and confirm your login.</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>This app needs access to your location to provide you with the best ride experience.
|
<string>Intaleq Rider uses your location to show available drivers nearby, automatically set pickup points, provide turn-by-turn tracking, and update driver arrival status even when the app is in the background.</string>
|
||||||
Your location data will be used to find the nearest available cars and connect you with
|
|
||||||
the closest captain for efficient and convenient rides.</string>
|
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
<string>This app needs access to location.</string>
|
<string>Intaleq Rider uses your location in the background to maintain accurate live tracking for your active trips and notify you when your driver arrives.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>This app needs access to your location to provide you with the best ride experience.
|
<string>Intaleq Rider uses your location while using the app to locate nearby drivers, calculate trip fare estimates, and set your pickup and drop-off points.</string>
|
||||||
Your location data will be used to find the nearest available cars and connect you with
|
|
||||||
the closest captain for efficient and convenient rides.</string>
|
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>This app requires access to your microphone to record audio, allowing you to add
|
<string>Intaleq Rider uses your microphone to allow direct in-app voice calls with your driver for pickup coordination and to record audio notes for support tickets.</string>
|
||||||
voice recordings to your photos and videos and access to connect to a call.</string>
|
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>This app requires access to the photo library to upload pictures.</string>
|
<string>Intaleq Rider accesses your photo library so you can select and upload a profile avatar or attach screenshots when submitting support requests.</string>
|
||||||
<key>NSSupportsLiveActivities</key>
|
<key>NSSupportsLiveActivities</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
|||||||
@@ -82,26 +82,55 @@ class LoginController extends GetxController {
|
|||||||
// • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime
|
// • firstTimeLoadKey != false ← أول مرة يفتح التطبيق → loginFirstTime
|
||||||
// • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider
|
// • firstTimeLoadKey == false ← مستخدم موجود → loginJwtRider
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
Future<void> getJWT({bool force = false}) async {
|
static Future<bool>? _jwtFuture;
|
||||||
// إذا كان التوكن الحالي لا يزال صالحاً، لا داعي لطلب واحد جديد
|
static DateTime _jwtCooldownUntil = DateTime(2000);
|
||||||
|
static int _jwtFailures = 0;
|
||||||
|
|
||||||
|
static Duration get jwtCooldownRemaining {
|
||||||
|
final d = _jwtCooldownUntil.difference(DateTime.now());
|
||||||
|
return d.isNegative ? Duration.zero : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> getJWT({bool force = false}) async {
|
||||||
|
if (_jwtFuture != null) {
|
||||||
|
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
|
||||||
|
return _jwtFuture!;
|
||||||
|
}
|
||||||
|
_jwtFuture = _getJwtInternal(force: force).catchError((e) {
|
||||||
|
return _onJwtFailure('exception: $e');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return await _jwtFuture!;
|
||||||
|
} finally {
|
||||||
|
_jwtFuture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _getJwtInternal({bool force = false}) async {
|
||||||
if (!force && isTokenValid()) {
|
if (!force && isTokenValid()) {
|
||||||
Log.print("JWT is still valid. Skipping request.");
|
Log.print("JWT is still valid. Skipping request.");
|
||||||
return;
|
_jwtFailures = 0;
|
||||||
|
_jwtCooldownUntil = DateTime(2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
|
||||||
|
Log.print(
|
||||||
|
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||||
|
|
||||||
// تأكد إن البصمة محدّثة قبل أي طلب
|
|
||||||
await DeviceHelper.getDeviceFingerprint();
|
await DeviceHelper.getDeviceFingerprint();
|
||||||
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
|
final String fp = box.read(BoxName.deviceFpEncrypted) ?? '';
|
||||||
|
|
||||||
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
final passengerId = box.read(BoxName.passengerID);
|
||||||
// ── أول تسجيل ─────────────────────────────────────────
|
final isRegistering = passengerId == null || passengerId.toString().isEmpty;
|
||||||
// نرسل البصمة المشفرة مع باقي البيانات
|
|
||||||
// السيرفر سيعمل hash لها ويخزنها في JWT payload
|
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.passengerID) ?? AK.newId,
|
'id': passengerId ?? AK.newId,
|
||||||
'password': AK.passnpassenger,
|
'password': AK.passnpassenger,
|
||||||
'aud': '${AK.allowed}$dev',
|
'aud': '${AK.allowed}$dev',
|
||||||
'fingerPrint': fp,
|
'fingerPrint': fp,
|
||||||
@@ -110,12 +139,14 @@ class LoginController extends GetxController {
|
|||||||
var response = await http.post(
|
var response = await http.post(
|
||||||
Uri.parse(AppLink.loginFirstTime),
|
Uri.parse(AppLink.loginFirstTime),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
).timeout(const Duration(seconds: 30));
|
||||||
Log.print('AppLink.loginFirstTime: ${AppLink.loginFirstTime}');
|
|
||||||
|
|
||||||
Log.print('payload: $payload');
|
if (response.statusCode == 429) {
|
||||||
Log.print('response code: ${response.statusCode}');
|
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
|
||||||
Log.print('response body: ${response.body}');
|
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
|
||||||
|
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = jsonDecode(response.body);
|
final decoded = jsonDecode(response.body);
|
||||||
@@ -126,18 +157,20 @@ class LoginController extends GetxController {
|
|||||||
: decoded['jwt']);
|
: decoded['jwt']);
|
||||||
|
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
// نشفر الـ JWT بالتشفير الثلاثي قبل التخزين في GetStorage
|
await storage.write(key: BoxName.jwt, value: jwt);
|
||||||
box.write(BoxName.jwt, c(jwt));
|
|
||||||
storage.write(key: BoxName.jwt, value: c(jwt));
|
|
||||||
}
|
|
||||||
|
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
return _onJwtSuccess();
|
||||||
}
|
}
|
||||||
|
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
|
||||||
|
}
|
||||||
|
return _onJwtFailure('firstTime: HTTP ${response.statusCode}');
|
||||||
} else {
|
} else {
|
||||||
// ── مستخدم موجود: تجديد التوكن
|
if (isRegistering) {
|
||||||
|
return _onJwtFailure('renew: لا يوجد passengerID');
|
||||||
|
}
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
'id': box.read(BoxName.passengerID),
|
'id': passengerId,
|
||||||
'fingerPrint': fp,
|
'fingerPrint': fp,
|
||||||
'aud': '${AK.allowed}$dev',
|
'aud': '${AK.allowed}$dev',
|
||||||
};
|
};
|
||||||
@@ -145,11 +178,15 @@ class LoginController extends GetxController {
|
|||||||
var response = await http.post(
|
var response = await http.post(
|
||||||
Uri.parse(AppLink.loginJwtRider),
|
Uri.parse(AppLink.loginJwtRider),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
).timeout(const Duration(seconds: 30));
|
||||||
Log.print('AppLink.loginJwtRider: ${AppLink.loginJwtRider}');
|
|
||||||
|
if (response.statusCode == 429) {
|
||||||
|
final retryAfter = int.tryParse(response.headers['retry-after'] ?? '') ?? 60;
|
||||||
|
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
|
||||||
|
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Log.print('payload: $payload');
|
|
||||||
Log.print('response: ${response.body}');
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = jsonDecode(response.body);
|
final decoded = jsonDecode(response.body);
|
||||||
final String? jwt = decoded['data'] != null
|
final String? jwt = decoded['data'] != null
|
||||||
@@ -159,16 +196,33 @@ class LoginController extends GetxController {
|
|||||||
: decoded['jwt']);
|
: decoded['jwt']);
|
||||||
|
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
box.write(BoxName.jwt, c(jwt));
|
await storage.write(key: BoxName.jwt, value: jwt);
|
||||||
storage.write(key: BoxName.jwt, value: c(jwt));
|
return _onJwtSuccess();
|
||||||
}
|
}
|
||||||
|
return _onJwtFailure('renew: لا يوجد jwt بالرد');
|
||||||
}
|
}
|
||||||
|
return _onJwtFailure('renew: HTTP ${response.statusCode}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.print('Error in getJWT: $e');
|
return _onJwtFailure('Error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _onJwtSuccess() {
|
||||||
|
_jwtFailures = 0;
|
||||||
|
_jwtCooldownUntil = DateTime(2000);
|
||||||
|
Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onJwtFailure(String reason) {
|
||||||
|
_jwtFailures++;
|
||||||
|
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
|
||||||
|
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
|
||||||
|
Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
// التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية)
|
// التحقق من صلاحية التوكن يدوياً (بدون مكاتب خارجية)
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class CRUD {
|
|||||||
final NetGuard _netGuard = NetGuard();
|
final NetGuard _netGuard = NetGuard();
|
||||||
final _client = SslPinning.createPinnedClient();
|
final _client = SslPinning.createPinnedClient();
|
||||||
|
|
||||||
static bool _isRefreshingJWT = false;
|
|
||||||
static String _lastErrorSignature = '';
|
static String _lastErrorSignature = '';
|
||||||
static DateTime _lastErrorTimestamp = DateTime(2000);
|
static DateTime _lastErrorTimestamp = DateTime(2000);
|
||||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||||
@@ -98,31 +97,50 @@ class CRUD {
|
|||||||
|
|
||||||
Future<String> _getJwt() async {
|
Future<String> _getJwt() async {
|
||||||
try {
|
try {
|
||||||
final String? encryptedJwt = await storage.read(key: BoxName.jwt);
|
final jwt = await storage.read(key: BoxName.jwt);
|
||||||
if (encryptedJwt == null || encryptedJwt.isEmpty) {
|
if (jwt == null || jwt.toString().isEmpty) {
|
||||||
final String? fallback = box.read(BoxName.jwt);
|
// إذا كان التخزين الآمن فارغاً، نحاول استخراج التوكن القديم من GetStorage للركاب القدامى
|
||||||
|
final fallback = box.read(BoxName.jwt);
|
||||||
if (fallback != null) {
|
if (fallback != null) {
|
||||||
return r(fallback).toString().split(Env.addd)[0];
|
try {
|
||||||
|
return r(fallback).toString().split(Env.addd)[0]; // فك تشفير القديم
|
||||||
|
} catch (_) {
|
||||||
|
return fallback.toString(); // ربما تم تخزينه بدون تشفير
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return r(encryptedJwt).toString().split(Env.addd)[0];
|
// التحقق السريع إذا كان التوكن لا يزال مشفراً (يبدأ برموز غريبة وليس ey)
|
||||||
} catch (e) {
|
if (!jwt.startsWith('ey')) {
|
||||||
Log.print('Error reading JWT from SecureStorage: $e');
|
try {
|
||||||
final String? fallback = box.read(BoxName.jwt);
|
return r(jwt).toString().split(Env.addd)[0];
|
||||||
if (fallback != null) {
|
} catch (_) {}
|
||||||
return r(fallback).toString().split(Env.addd)[0];
|
|
||||||
}
|
}
|
||||||
|
return jwt;
|
||||||
|
} catch (_) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
Future<String> _ensureJwt() async {
|
||||||
|
String token = await _getJwt();
|
||||||
|
if (_isJwtValid(token)) return token;
|
||||||
|
|
||||||
|
final ok = await Get.put(LoginController()).getJWT();
|
||||||
|
if (!ok) return '';
|
||||||
|
return await _getJwt();
|
||||||
|
}
|
||||||
|
|
||||||
/// Centralized request handler with retry for weak networks.
|
/// Centralized request handler with retry for weak networks.
|
||||||
/// For Syria (3G): 60s total timeout, 3 retries, exponential backoff.
|
/// For Syria (3G): 60s total timeout, 3 retries, exponential backoff.
|
||||||
Future<dynamic> _makeRequest({
|
Future<dynamic> _makeRequest({
|
||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
required Map<String, String> headers,
|
required Map<String, String> headers,
|
||||||
|
bool allowRefresh = true,
|
||||||
}) async {
|
}) async {
|
||||||
const totalTimeout = Duration(seconds: 60);
|
const totalTimeout = Duration(seconds: 60);
|
||||||
|
|
||||||
@@ -180,17 +198,33 @@ class CRUD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
|
||||||
|
if (sc == 429) {
|
||||||
|
Log.print('🛑 [RES] 429 rate limited — $link');
|
||||||
|
return 'rate_limited';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
|
||||||
if (sc == 401) {
|
if (sc == 401) {
|
||||||
|
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
||||||
final isNonCritical = link.contains('errorApp.php');
|
final isNonCritical = link.contains('errorApp.php');
|
||||||
if (!_isRefreshingJWT && !isNonCritical) {
|
if (isNonCritical || !allowRefresh) return 'token_expired';
|
||||||
_isRefreshingJWT = true;
|
|
||||||
try {
|
final refreshed = await Get.put(LoginController()).getJWT();
|
||||||
await Get.put(LoginController()).getJWT();
|
if (!refreshed) return 'token_expired';
|
||||||
} finally {
|
|
||||||
_isRefreshingJWT = false;
|
final newToken = await _getJwt();
|
||||||
}
|
if (newToken.isEmpty) return 'token_expired';
|
||||||
}
|
|
||||||
return 'token_expired';
|
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
|
||||||
|
final retryHeaders = Map<String, String>.from(headers)
|
||||||
|
..['Authorization'] = 'Bearer $newToken';
|
||||||
|
return await _makeRequest(
|
||||||
|
link: link,
|
||||||
|
payload: payload,
|
||||||
|
headers: retryHeaders,
|
||||||
|
allowRefresh: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sc >= 500) {
|
if (sc >= 500) {
|
||||||
@@ -206,7 +240,14 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
String token = await _getJwt();
|
String token = await _ensureJwt();
|
||||||
|
if (token.isEmpty) {
|
||||||
|
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
|
||||||
|
// باستثناء تسجيل الدخول لأنه لا يحتاج توكن
|
||||||
|
if (!link.contains('login') && !link.contains('errorApp.php')) {
|
||||||
|
return 'token_expired';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
@@ -221,7 +262,12 @@ class CRUD {
|
|||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
String token = await _getJwt();
|
String token = await _ensureJwt();
|
||||||
|
if (token.isEmpty) {
|
||||||
|
if (!link.contains('login') && !link.contains('errorApp.php')) {
|
||||||
|
return 'token_expired';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final headers = {
|
final headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class MapSocketController extends GetxController {
|
|||||||
io_client.OptionBuilder()
|
io_client.OptionBuilder()
|
||||||
.setTransports(['websocket'])
|
.setTransports(['websocket'])
|
||||||
.disableAutoConnect()
|
.disableAutoConnect()
|
||||||
.setQuery({'id': passengerId, 'jwt': jwt})
|
.setQuery({'id': passengerId, 'jwt': jwt, 'EIO': '3'})
|
||||||
// محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart).
|
// محاولات لا نهائية، توحيداً مع تطبيق السائق (background_service.dart).
|
||||||
// كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق
|
// كانت 20: بعدها يستسلم السوكيت **نهائياً** فيصمت للأبد بعد بضع دقائق
|
||||||
// من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال
|
// من شبكة سيئة، ويبقى الراكب على الـ polling بلا أن يدري. صار الاتصال
|
||||||
@@ -173,18 +173,6 @@ class MapSocketController extends GetxController {
|
|||||||
socket.on('connect_error', (error) {
|
socket.on('connect_error', (error) {
|
||||||
Log.print("❌ Socket Connect Error: $error");
|
Log.print("❌ Socket Connect Error: $error");
|
||||||
isSocketConnected = false;
|
isSocketConnected = false;
|
||||||
// في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error
|
|
||||||
// نتأكد يدوياً من إعادة الاتصال
|
|
||||||
Future.delayed(const Duration(seconds: 3), () {
|
|
||||||
if (!isSocketConnected && _isSocketInitialized) {
|
|
||||||
Log.print("🔄 Manual reconnect after connect_error...");
|
|
||||||
try {
|
|
||||||
socket.connect();
|
|
||||||
} catch (e) {
|
|
||||||
Log.print("Manual reconnect error: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('ride_status_change', (data) {
|
socket.on('ride_status_change', (data) {
|
||||||
|
|||||||
@@ -47,19 +47,21 @@ class DefaultFirebaseOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const FirebaseOptions android = FirebaseOptions(
|
static const FirebaseOptions android = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyAEoply_UcEP6KaCu_ziCy_ZDIjAKvi7b8',
|
apiKey: 'AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64',
|
||||||
appId: '1:825988584191:android:7e9088b719dcb7061632ca',
|
appId: '1:1086900987150:android:b7231956aa6d3b3377a35f',
|
||||||
messagingSenderId: '825988584191',
|
messagingSenderId: '1086900987150',
|
||||||
projectId: 'siro-a6957',
|
projectId: 'intaleq-d48a7',
|
||||||
storageBucket: 'siro-a6957.firebasestorage.app',
|
storageBucket: 'intaleq-d48a7.firebasestorage.app',
|
||||||
);
|
);
|
||||||
static const FirebaseOptions ios = FirebaseOptions(
|
static const FirebaseOptions ios = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
|
apiKey: 'AIzaSyDzGO9a-1IDMLD2FxhmOO9ONL1gMssFa9g',
|
||||||
appId: '1:825988584191:ios:4f60966dd0a69b3f1632ca',
|
appId: '1:1086900987150:ios:a60973accd7f3dc777a35f',
|
||||||
messagingSenderId: '825988584191',
|
messagingSenderId: '1086900987150',
|
||||||
projectId: 'siro-a6957',
|
projectId: 'intaleq-d48a7',
|
||||||
storageBucket: 'siro-a6957.firebasestorage.app',
|
storageBucket: 'intaleq-d48a7.firebasestorage.app',
|
||||||
iosBundleId: 'com.siroapp.rider',
|
androidClientId: '1086900987150-060srlmdjocdcav377rbur4ka14m90b7.apps.googleusercontent.com',
|
||||||
|
iosClientId: '1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d.apps.googleusercontent.com',
|
||||||
|
iosBundleId: 'com.Intaleq.intaleq',
|
||||||
);
|
);
|
||||||
static const FirebaseOptions macos = FirebaseOptions(
|
static const FirebaseOptions macos = FirebaseOptions(
|
||||||
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
|
apiKey: 'AIzaSyDk6x6KZUY0IQtxoCMXX0F7N_yik8O19eA',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: intaleq_rider
|
|||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
version: 2.0.0+66
|
version: 34.0.0+67
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.5 <4.0.0"
|
sdk: ">=3.0.5 <4.0.0"
|
||||||
@@ -94,6 +94,7 @@ dev_dependencies:
|
|||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: "launcher_icon"
|
android: "launcher_icon"
|
||||||
ios: true
|
ios: true
|
||||||
|
remove_alpha_ios: true
|
||||||
image_path: "assets/images/logo.png"
|
image_path: "assets/images/logo.png"
|
||||||
min_sdk_android: 21
|
min_sdk_android: 21
|
||||||
web:
|
web:
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ function getInternalKey(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRedisPass(): ?string {
|
function getRedisPass(): ?string {
|
||||||
|
$envPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
|
||||||
|
if ($envPass) return trim($envPass);
|
||||||
$path = getenv('REDIS_PASS_KEY_PATH');
|
$path = getenv('REDIS_PASS_KEY_PATH');
|
||||||
if ($path && file_exists($path)) return trim((string)@file_get_contents($path));
|
if ($path && file_exists($path)) return trim((string)@file_get_contents($path));
|
||||||
if (file_exists('/keys/.reds_pass_key')) return trim((string)@file_get_contents('/keys/.reds_pass_key'));
|
if (file_exists('/keys/.reds_pass_key')) return trim((string)@file_get_contents('/keys/.reds_pass_key'));
|
||||||
@@ -31,6 +33,7 @@ $headers = getallheaders();
|
|||||||
$receivedKey = $headers['x-internal-key'] ?? $_SERVER['HTTP_X_INTERNAL_KEY'] ?? '';
|
$receivedKey = $headers['x-internal-key'] ?? $_SERVER['HTTP_X_INTERNAL_KEY'] ?? '';
|
||||||
|
|
||||||
if (!empty($INTERNAL_KEY) && $receivedKey !== $INTERNAL_KEY) {
|
if (!empty($INTERNAL_KEY) && $receivedKey !== $INTERNAL_KEY) {
|
||||||
|
error_log("[api_get_nearby] Unauthorized: receivedKey ($receivedKey) != expectedKey ($INTERNAL_KEY)");
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['status' => false, 'msg' => 'Unauthorized']);
|
echo json_encode(['status' => false, 'msg' => 'Unauthorized']);
|
||||||
exit;
|
exit;
|
||||||
@@ -79,7 +82,15 @@ try {
|
|||||||
$profile = $redis->hgetall("driver:profile:$d_id");
|
$profile = $redis->hgetall("driver:profile:$d_id");
|
||||||
$lastUpdate = isset($profile['updated_at']) ? (int)$profile['updated_at'] : 0;
|
$lastUpdate = isset($profile['updated_at']) ? (int)$profile['updated_at'] : 0;
|
||||||
|
|
||||||
if (empty($profile) || ($currentTime - $lastUpdate) > $max_silence) {
|
if (empty($profile)) {
|
||||||
|
error_log("[api_get_nearby] Driver $d_id skipped: profile is empty in Redis.");
|
||||||
|
$redis->zrem('geo:drivers:available', $d_id);
|
||||||
|
$redis->zrem('geo:drivers:busy', $d_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($currentTime - $lastUpdate) > $max_silence) {
|
||||||
|
error_log("[api_get_nearby] Driver $d_id skipped: lastUpdate ($lastUpdate) is older than max_silence ($max_silence).");
|
||||||
$redis->zrem('geo:drivers:available', $d_id);
|
$redis->zrem('geo:drivers:available', $d_id);
|
||||||
$redis->zrem('geo:drivers:busy', $d_id);
|
$redis->zrem('geo:drivers:busy', $d_id);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -90,19 +90,24 @@ function getJwtSecret(): string {
|
|||||||
// Redis password: try env var, then Docker keys, then legacy path, then null (Docker Redis has no password)
|
// Redis password: try env var, then Docker keys, then legacy path, then null (Docker Redis has no password)
|
||||||
$redisPass = null;
|
$redisPass = null;
|
||||||
$redisPassPaths = [
|
$redisPassPaths = [
|
||||||
getenv('REDIS_PASS_KEY_PATH') ?: '',
|
__DIR__ . '/../loction-keys/.reds_pass_key',
|
||||||
'/keys/.reds_pass_key',
|
'/keys/.reds_pass_key',
|
||||||
'/home/location/.reds_pass_key',
|
'/home/location/.reds_pass_key'
|
||||||
];
|
];
|
||||||
foreach ($redisPassPaths as $rpp) {
|
foreach ($redisPassPaths as $rpp) {
|
||||||
if (!empty($rpp) && file_exists($rpp)) {
|
if (file_exists($rpp)) {
|
||||||
$redisPass = trim((string) @file_get_contents($rpp));
|
$redisPass = trim((string) @file_get_contents($rpp));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (empty($redisPass)) {
|
||||||
|
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($INTERNAL_KEY)) logMsg('⚠️ Internal key not found (non-critical in Docker)');
|
if (empty($INTERNAL_KEY)) logMsg('⚠️ Internal key not found (non-critical in Docker)');
|
||||||
if (empty($redisPass)) logMsg('ℹ️ Redis password not set (OK for Docker — no auth required)');
|
if (empty($redisPass)) logMsg('ℹ️ Redis password not set (OK for Docker — no auth required)');
|
||||||
|
else logMsg('✅ Redis password loaded successfully');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 🗄️ Redis Singleton
|
// 🗄️ Redis Singleton
|
||||||
@@ -368,8 +373,10 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
|
|||||||
|
|
||||||
if ($st === 'off') {
|
if ($st === 'off') {
|
||||||
$pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId);
|
$pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId);
|
||||||
|
logMsg("[REDIS BATCH] Added Driver $driverId to geo:drivers:available at $lat, $lng");
|
||||||
} elseif ($st === 'on') {
|
} elseif ($st === 'on') {
|
||||||
$pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId);
|
$pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId);
|
||||||
|
logMsg("[REDIS BATCH] Added Driver $driverId to geo:drivers:busy at $lat, $lng");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -877,6 +884,8 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
|
|||||||
$state = &$driverState[$driverId];
|
$state = &$driverState[$driverId];
|
||||||
$now = time();
|
$now = time();
|
||||||
|
|
||||||
|
logMsg("[SOCKET] update_location received for Driver $driverId | status=$status, lat=$lat, lng=$lng");
|
||||||
|
|
||||||
// 1. Forward للراكب (ASYNC + throttle)
|
// 1. Forward للراكب (ASYNC + throttle)
|
||||||
if (!empty($passengerId)) {
|
if (!empty($passengerId)) {
|
||||||
forwardLocationToPassengerSocket(
|
forwardLocationToPassengerSocket(
|
||||||
|
|||||||
@@ -75,10 +75,13 @@ function getLocationRedis(): ?\Redis {
|
|||||||
catch (\Exception $_) { $_locationRedis = null; }
|
catch (\Exception $_) { $_locationRedis = null; }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$redisPass = '';
|
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: ($_ENV['REDIS_MAIN_PASSWORD'] ?? (getenv('REDIS_PASSWORD') ?: ($_ENV['REDIS_PASSWORD'] ?? null)));
|
||||||
foreach ([getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'] as $p) {
|
$keys = [getenv('REDIS_PASS_KEY_PATH') ?: '', '/keys/.reds_pass_key', '/home/location/.reds_pass_key'];
|
||||||
|
if (!$redisPass) {
|
||||||
|
foreach ($keys as $p) {
|
||||||
if (!empty($p) && file_exists($p)) { $redisPass = trim((string)@file_get_contents($p)); break; }
|
if (!empty($p) && file_exists($p)) { $redisPass = trim((string)@file_get_contents($p)); break; }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$host = getenv('REDIS_HOST') ?: (file_exists('/.dockerenv') ? 'redis' : '127.0.0.1');
|
$host = getenv('REDIS_HOST') ?: (file_exists('/.dockerenv') ? 'redis' : '127.0.0.1');
|
||||||
$r = new \Redis();
|
$r = new \Redis();
|
||||||
$r->connect($host, (int)(getenv('REDIS_PORT') ?: 6379), 1.5);
|
$r->connect($host, (int)(getenv('REDIS_PORT') ?: 6379), 1.5);
|
||||||
|
|||||||