From a547b81b60da4436002922bf16a92d2e69291d39 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 14 Jul 2026 00:34:22 +0300 Subject: [PATCH] Update: 2026-07-14 00:34:22 --- backend/transit/admin/logout.php | 9 +++- backend/transit/broadcast/send.php | 7 ++++ backend/transit/cron_cleanup.php | 36 ++++++++++++++++ backend/transit/driver/activate.php | 41 ------------------ backend/transit/driver/activate_by_app.php | 3 ++ backend/transit/driver/suspend.php | 7 +++- backend/transit/functions.php | 48 ++++++++++++++++++++++ backend/transit/schema_transit.sql | 5 ++- backend/transit/trip/board_passenger.php | 8 +--- backend/transit/trip/delay.php | 8 +--- backend/transit/trip/end.php | 8 +--- backend/transit/trip/start.php | 8 +--- backend/transit/trip/stop_passengers.php | 8 +--- backend/transit/trip/today.php | 8 +--- 14 files changed, 123 insertions(+), 81 deletions(-) create mode 100644 backend/transit/cron_cleanup.php delete mode 100644 backend/transit/driver/activate.php diff --git a/backend/transit/admin/logout.php b/backend/transit/admin/logout.php index 10898a8a..25b462be 100644 --- a/backend/transit/admin/logout.php +++ b/backend/transit/admin/logout.php @@ -5,6 +5,10 @@ require_once __DIR__ . '/../../core/bootstrap.php'; require_once __DIR__ . '/../functions.php'; +// حد معدّل — يمنع إغراق حذف الجلسات (يحمّل bootstrap مباشرة كباقي مسارات admin/) +$limiter = new RateLimiter($redis); +$limiter->enforce(RateLimiter::identifier(), 'api'); + try { $transit_con = Database::get('transit'); } catch (Exception $e) { jsonError('Transit service unavailable', 503); } @@ -21,8 +25,9 @@ if (!$token) jsonError('No session token provided', 400); $hash = hash('sha256', $token); -// حذف من Redis -if ($redis) $redis->del("siro:transit:session:{$hash}"); +// حذف من Redis — بلا بادئة siro: يدوية (العميل يضيفها تلقائياً عبر OPT_PREFIX) +// كتابة "siro:transit:..." هنا كانت تنتج siro:siro:... فلا تُحذف الجلسة فعلياً +if ($redis) $redis->del("transit:session:{$hash}"); // حذف من MySQL try { diff --git a/backend/transit/broadcast/send.php b/backend/transit/broadcast/send.php index ca53000f..e9dccff5 100644 --- a/backend/transit/broadcast/send.php +++ b/backend/transit/broadcast/send.php @@ -12,6 +12,13 @@ $targetId = filterRequest('target_id', 'int') ?: null; if (!in_array($targetType, ['all', 'route'])) $targetType = 'all'; +// عند استهداف خط: تأكد أن الخط يخص مؤسسة هذا المشرف — يمنع البث لخط مؤسسة أخرى +if ($targetType === 'route' && $targetId) { + $chkRoute = $transit_con->prepare("SELECT id FROM transit_routes WHERE id=? AND org_id=? LIMIT 1"); + $chkRoute->execute([$targetId, $transit_org_id]); + if (!$chkRoute->fetch()) jsonError('Route not found or access denied', 404); +} + $fcmTopic = ($targetType === 'route' && $targetId) ? transitRouteTopic($targetId) : transitOrgTopic($transit_org_id); diff --git a/backend/transit/cron_cleanup.php b/backend/transit/cron_cleanup.php new file mode 100644 index 00000000..fc73fbf7 --- /dev/null +++ b/backend/transit/cron_cleanup.php @@ -0,0 +1,36 @@ +> /var/log/transit_cleanup.log 2>&1 + */ + +require_once __DIR__ . '/../core/bootstrap.php'; + +echo "[" . date('Y-m-d H:i:s') . "] Starting transit cleanup...\n"; + +try { + $transit_con = Database::get('transit'); +} catch (Exception $e) { + echo "[ERROR] Cannot connect to siroTransitDb: " . $e->getMessage() . "\n"; + exit(1); +} + +// 1. جلسات المشرفين المنتهية +$sessions = $transit_con->prepare("DELETE FROM transit_sessions WHERE expires_at < NOW()"); +$sessions->execute(); +echo "[CLEANUP] Expired sessions removed: " . $sessions->rowCount() . "\n"; + +// 2. سجل OTP المنتهي أو المستخدَم (أبقِ يوماً واحداً للتدقيق) +$otp = $transit_con->prepare( + "DELETE FROM transit_otp_log WHERE expires_at < (NOW() - INTERVAL 1 DAY) OR (used = 1 AND created_at < (NOW() - INTERVAL 1 DAY))" +); +$otp->execute(); +echo "[CLEANUP] Old OTP log rows removed: " . $otp->rowCount() . "\n"; + +echo "[" . date('Y-m-d H:i:s') . "] Cleanup done.\n"; diff --git a/backend/transit/driver/activate.php b/backend/transit/driver/activate.php deleted file mode 100644 index 460fd705..00000000 --- a/backend/transit/driver/activate.php +++ /dev/null @@ -1,41 +0,0 @@ -encryptData($phone); - -$st = $transit_con->prepare( - "SELECT id, org_id, name, status FROM transit_drivers - WHERE invite_token=? AND phone=? LIMIT 1" -); -$st->execute([$inviteToken, $phoneEnc]); -$driver = $st->fetch(); - -if (!$driver) jsonError('Invalid invite token or phone mismatch', 404); -if ($driver['status'] === 'active') jsonError('Driver already activated', 409); -if ($driver['status'] === 'suspended') jsonError('Account suspended', 403); - -$transit_con->prepare( - "UPDATE transit_drivers SET status='active', activated_at=NOW(), invite_token=NULL WHERE id=?" -)->execute([$driver['id']]); - -jsonSuccess([ - 'driver_id' => (int)$driver['id'], - 'org_id' => (int)$driver['org_id'], - 'name' => $driver['name'], - 'mode' => 'bus', -], 'Driver activated. Welcome to مواصلاتي!'); diff --git a/backend/transit/driver/activate_by_app.php b/backend/transit/driver/activate_by_app.php index 74b38057..53ede72c 100644 --- a/backend/transit/driver/activate_by_app.php +++ b/backend/transit/driver/activate_by_app.php @@ -51,6 +51,9 @@ $transit_con->prepare( WHERE id=?" )->execute([(string)$transit_user_id, $driver['id']]); +// إبطال أي كاش قديم لهذا الحساب (قد يكون كُوش كـ null سابقاً أو بحالة أخرى) +transitInvalidateDriverCache((string)$transit_user_id); + appLog("[TRANSIT][DRIVER] activated driver #{$driver['id']} (org#{$driver['org_id']}) linked to main_driver_id={$transit_user_id}", 'INFO'); jsonSuccess([ diff --git a/backend/transit/driver/suspend.php b/backend/transit/driver/suspend.php index be2523e4..c31d4447 100644 --- a/backend/transit/driver/suspend.php +++ b/backend/transit/driver/suspend.php @@ -9,7 +9,7 @@ $action = filterRequest('action'); if (!$driverId) jsonError('driver_id is required', 400); if (!in_array($action, ['suspend', 'activate'])) jsonError('action must be suspend or activate', 400); -$chk = $transit_con->prepare("SELECT id, status FROM transit_drivers WHERE id=? AND org_id=? LIMIT 1"); +$chk = $transit_con->prepare("SELECT id, status, main_driver_id FROM transit_drivers WHERE id=? AND org_id=? LIMIT 1"); $chk->execute([$driverId, $transit_org_id]); $driver = $chk->fetch(); if (!$driver) jsonError('Driver not found or access denied', 404); @@ -22,6 +22,11 @@ if ($action === 'activate' && $driver['status'] !== 'suspended') { $newStatus = $action === 'suspend' ? 'suspended' : 'active'; $transit_con->prepare("UPDATE transit_drivers SET status=? WHERE id=?")->execute([$newStatus, $driverId]); +// إبطال كاش السائق فوراً — عند التعليق يجب أن تُرفض رحلاته في الطلب التالي مباشرة +if (!empty($driver['main_driver_id'])) { + transitInvalidateDriverCache((string)$driver['main_driver_id']); +} + appLog("[TRANSIT][DRIVER][suspend] driver #{$driverId} org#{$transit_org_id} → {$newStatus}"); jsonSuccess(['driver_id' => $driverId, 'status' => $newStatus]); diff --git a/backend/transit/functions.php b/backend/transit/functions.php index 3c3f6a28..6c96dbe7 100644 --- a/backend/transit/functions.php +++ b/backend/transit/functions.php @@ -252,6 +252,54 @@ function transitDropEnrollment(int $orgId, string $passengerId): void $redisLocation->sRem("transit:org_members:{$orgId}", $passengerId); } +// ── كاش السائق في Redis ($redis الرئيسي — بادئة siro:) ────────── +// جدول transit_drivers صغير، لكن endpoints الرحلات تستعلم عنه بكثرة +// (today, start, end, delay, board_passenger, stop_passengers) — خصوصاً +// board_passenger أثناء مسح تذاكر الصعود. نمط read-through: نقرأ من Redis +// أولاً، وإن لم يوجد نستعلم القاعدة ونخزّن. المفتاح بـ main_driver_id +// (المعرّف الآتي من JWT). نبطل الكاش عند أي تغيير حالة (تفعيل/تعليق). +// +// نخزّن فقط السائقين النشطين (status='active'). أي سجل غير نشط لا يُكاش +// حتى لا نحتاج إبطالاً عند التفعيل — أول استعلام بعد التفعيل يبني الكاش. + +function transitResolveDriver(string $mainDriverId): ?array +{ + global $redis, $transit_con; + if (!$mainDriverId) return null; + + $key = "transit:driver:{$mainDriverId}"; + + if ($redis) { + $cached = $redis->hGetAll($key); + if (!empty($cached) && isset($cached['id'])) { + return ['id' => (int)$cached['id'], 'org_id' => (int)$cached['org_id']]; + } + } + + if (!$transit_con) $transit_con = Database::get('transit'); + $st = $transit_con->prepare( + "SELECT id, org_id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" + ); + $st->execute([$mainDriverId]); + $driver = $st->fetch(); + if (!$driver) return null; + + $resolved = ['id' => (int)$driver['id'], 'org_id' => (int)$driver['org_id']]; + + if ($redis) { + $redis->hMSet($key, $resolved); + $redis->expire($key, 3600); // ساعة — طبقة حماية إن فات إبطالٌ ما + } + + return $resolved; +} + +function transitInvalidateDriverCache(string $mainDriverId): void +{ + global $redis; + if ($redis && $mainDriverId) $redis->del("transit:driver:{$mainDriverId}"); +} + // ── days_mask helpers ──────────────────────────────────────── // bit0=Sun … bit6=Sat | 62 = 0b0111110 = الأحد–الخميس diff --git a/backend/transit/schema_transit.sql b/backend/transit/schema_transit.sql index 46abd12a..727677b3 100644 --- a/backend/transit/schema_transit.sql +++ b/backend/transit/schema_transit.sql @@ -123,7 +123,8 @@ CREATE TABLE IF NOT EXISTS `transit_drivers` ( PRIMARY KEY (`id`), UNIQUE KEY `uq_phone_org` (`phone`, `org_id`), KEY `idx_org` (`org_id`), - KEY `idx_invite_token` (`invite_token`) + KEY `idx_invite_token` (`invite_token`), + KEY `idx_main_driver` (`main_driver_id`) -- يُستعلم في transitResolveDriver + driver/me.php ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- ----------------------------------------------------------------- @@ -339,6 +340,8 @@ SET FOREIGN_KEY_CHECKS = 1; -- COMMENT 'NULL إن كان الجدول بلا باص مخصص'; -- ALTER TABLE transit_routes MODIFY status ENUM('draft','active','suspended','rejected') NOT NULL DEFAULT 'draft'; -- (بدونها: Admin/transit/route/approve.php بإجراء reject يفشل بخطأ SQL دائماً) +-- ALTER TABLE transit_drivers ADD KEY idx_main_driver (main_driver_id); +-- (بدونها: كل استعلام transitResolveDriver / driver/me.php = مسح جدول كامل) -- ----------------------------------------------------------------- -- ----------------------------------------------------------------- diff --git a/backend/transit/trip/board_passenger.php b/backend/transit/trip/board_passenger.php index 432e2470..39fe1e7e 100644 --- a/backend/transit/trip/board_passenger.php +++ b/backend/transit/trip/board_passenger.php @@ -13,12 +13,8 @@ if (!$stopId || !$passengerId) { jsonError('Missing parameters', 400); } -// استخرج سجل السائق من JWT — لا نثق بأي معرّف سائق من العميل -$driverRow = $transit_con->prepare( - "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" -); -$driverRow->execute([(string)$transit_user_id]); -$driverRec = $driverRow->fetch(); +// استخرج سجل السائق من JWT (Redis read-through) — لا نثق بأي معرّف سائق من العميل +$driverRec = transitResolveDriver((string)$transit_user_id); if (!$driverRec) jsonError('Driver not found or not active', 403); $transit_driver_id = (int)$driverRec['id']; diff --git a/backend/transit/trip/delay.php b/backend/transit/trip/delay.php index e21596fa..ecdacc1c 100644 --- a/backend/transit/trip/delay.php +++ b/backend/transit/trip/delay.php @@ -10,12 +10,8 @@ $delay = min((int)filterRequest('delay_minutes'), 120); $reason = filterRequest('reason'); if (!$tripId || $delay <= 0) jsonError('trip_id and delay_minutes are required', 400); -// استخرج سجل السائق من JWT -$driverRow = $transit_con->prepare( - "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" -); -$driverRow->execute([(string)$transit_user_id]); -$driverRec = $driverRow->fetch(); +// استخرج سجل السائق من JWT (Redis read-through) +$driverRec = transitResolveDriver((string)$transit_user_id); if (!$driverRec) jsonError('Driver not found or not active', 403); $driverId = (int)$driverRec['id']; diff --git a/backend/transit/trip/end.php b/backend/transit/trip/end.php index f61317c1..a8aa9c96 100644 --- a/backend/transit/trip/end.php +++ b/backend/transit/trip/end.php @@ -8,12 +8,8 @@ if ($transit_user_role !== 'driver') jsonError('Only drivers can access this end $tripId = filterRequest('trip_id', 'int'); if (!$tripId) jsonError('trip_id is required', 400); -// استخرج سجل السائق من JWT -$driverRow = $transit_con->prepare( - "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" -); -$driverRow->execute([(string)$transit_user_id]); -$driverRec = $driverRow->fetch(); +// استخرج سجل السائق من JWT (Redis read-through) +$driverRec = transitResolveDriver((string)$transit_user_id); if (!$driverRec) jsonError('Driver not found or not active', 403); $driverId = (int)$driverRec['id']; diff --git a/backend/transit/trip/start.php b/backend/transit/trip/start.php index c13d63c4..f96cd54a 100644 --- a/backend/transit/trip/start.php +++ b/backend/transit/trip/start.php @@ -10,12 +10,8 @@ $tripId = filterRequest('trip_id', 'int'); $lat = (float)filterRequest('lat'); $lng = (float)filterRequest('lng'); -// استخرج سجل السائق من JWT — تحقق أن الرحلة تخصه -$driverRow = $transit_con->prepare( - "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" -); -$driverRow->execute([(string)$transit_user_id]); -$driverRec = $driverRow->fetch(); +// استخرج سجل السائق من JWT (Redis read-through) — تحقق أن الرحلة تخصه +$driverRec = transitResolveDriver((string)$transit_user_id); if (!$driverRec) jsonError('Driver not found or not active', 403); $driverId = (int)$driverRec['id']; diff --git a/backend/transit/trip/stop_passengers.php b/backend/transit/trip/stop_passengers.php index c4ddc25e..51cdc412 100644 --- a/backend/transit/trip/stop_passengers.php +++ b/backend/transit/trip/stop_passengers.php @@ -9,12 +9,8 @@ if ($transit_user_role !== 'driver') jsonError('Only drivers can access this end $stopId = (int)filterRequest('stop_id'); if (!$stopId) jsonError('Missing stop_id', 400); -// استخرج سجل السائق من JWT — لا نثق بأي معرّف سائق من العميل -$driverRow = $transit_con->prepare( - "SELECT id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" -); -$driverRow->execute([(string)$transit_user_id]); -$driverRec = $driverRow->fetch(); +// استخرج سجل السائق من JWT (Redis read-through) — لا نثق بأي معرّف سائق من العميل +$driverRec = transitResolveDriver((string)$transit_user_id); if (!$driverRec) jsonError('Driver not found or not active', 403); $transit_driver_id = (int)$driverRec['id']; diff --git a/backend/transit/trip/today.php b/backend/transit/trip/today.php index 95f59233..22635ac4 100644 --- a/backend/transit/trip/today.php +++ b/backend/transit/trip/today.php @@ -5,12 +5,8 @@ require_once __DIR__ . '/../../transit/connect_app.php'; if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403); -// استخرج سجل السائق من JWT — لا نثق بـ driver_transit_id من العميل -$chk = $transit_con->prepare( - "SELECT id, org_id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1" -); -$chk->execute([(string)$transit_user_id]); -$driver = $chk->fetch(); +// استخرج سجل السائق من JWT (Redis read-through) — لا نثق بـ driver_transit_id من العميل +$driver = transitResolveDriver((string)$transit_user_id); if (!$driver) jsonError('Driver not found or not active — activate your account first', 403); $driverTransitId = (int)$driver['id'];