prepare( "INSERT INTO transit_sessions (admin_id, org_id, token_hash, ip, user_agent, expires_at) VALUES (?,?,?,?,?,?)" )->execute([$adminId, $orgId, $hash, $ip, $ua, $expiresAt]); if ($redis) { $redis->setEx( "transit:session:{$hash}", 86400, json_encode(['admin_id' => $adminId, 'org_id' => $orgId]) ); } return $token; } function transitAuthAdmin(): array { global $redis; $header = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['HTTP_X_TRANSIT_TOKEN'] ?? ''; $token = str_replace('Bearer ', '', $header); if (!$token) jsonError('Missing admin session token', 401); $hash = hash('sha256', $token); if ($redis) { $val = $redis->get("transit:session:{$hash}"); if ($val) { $data = json_decode($val, true); if ($data) return $data; } jsonError('Session expired or invalid', 401); } // Fallback MySQL $con = Database::get('transit'); $st = $con->prepare( "SELECT admin_id, org_id FROM transit_sessions WHERE token_hash=? AND expires_at > NOW() LIMIT 1" ); $st->execute([$hash]); $row = $st->fetch(); if (!$row) jsonError('Session expired or invalid', 401); return $row; } // ── OTP مشرف الويب (يستخدم OtpService الموجود) ───────────── function transitSendAdminOtp(string $phone): void { global $redis; $otpSvc = new OtpService($redis); $otp = $otpSvc->generate($phone, 3); // 3 أرقام فقط (100–999) حسب سياسة مواصلاتي $message = "رمز تسجيل الدخول لمنصة مواصلاتي: {$otp}\nصالح لمدة 5 دقائق."; sendWhatsAppFromServer($phone, $message); } function transitVerifyAdminOtp(string $phone, string $otp): bool { global $redis; $otpSvc = new OtpService($redis); return $otpSvc->verify($phone, $otp); } function transitIsOtpLocked(string $phone): bool { global $redis; $otpSvc = new OtpService($redis); return $otpSvc->isLocked($phone); } // ── تشفير الرقم الجامعي / الوظيفي (يستخدم EncryptionHelper) ─ function transitEncryptStudentId(string $raw): string { global $encryptionHelper; return $encryptionHelper->encryptData($raw); } function transitDecryptStudentId(string $enc): string { global $encryptionHelper; return (string)$encryptionHelper->decryptData($enc); } // ── إشعارات FCM Topic للخطوط ──────────────────────────────── function transitRouteTopic(int $routeId): string { return 'transit_route_' . $routeId; } function transitOrgTopic(int $orgId): string { return 'transit_org_' . $orgId; } function transitSendTopicNotification(string $topic, string $title, string $body, array $data = []): void { global $redis; $fcm = new FcmService($redis); $result = $fcm->sendToTopic($topic, $title, $body, $data); if ($result['status'] !== 'success') { appLog("[TRANSIT][FCM] Topic push failed on '{$topic}': " . json_encode($result), 'WARNING'); } } // ── موقع الباص في Redis سيرفر الموقع ($redisLocation) ──────── // موقع الباص يكتبه driver_socket على Redis سيرفر الموقع (بدون بادئة siro:) // تماماً مثل موقع السائق العادي — لذلك نقرؤه/نكتبه عبر $redisLocation // وليس $redis الرئيسي (الذي يضيف بادئة siro: ولا يراه السوكت). function transitUpdateBusPosition(int $tripId, float $lat, float $lng): void { global $redisLocation; if (!$redisLocation) return; $redisLocation->hMSet("transit:trip:{$tripId}:pos", ['lat' => $lat, 'lng' => $lng, 'ts' => time()]); $redisLocation->expire("transit:trip:{$tripId}:pos", 86400); } function transitGetBusPosition(int $tripId): ?array { global $redisLocation; if (!$redisLocation) return null; $pos = $redisLocation->hGetAll("transit:trip:{$tripId}:pos"); return ($pos && isset($pos['lat'])) ? $pos : null; } // ── ملكية الرحلة (Trip Ownership) ───────────────────────────── // تُكتب عند بدء الرحلة على Redis سيرفر الموقع، ليتحقق منها driver_socket // قبل قبول أي update_bus_location — يمنع أي سائق آخر من انتحال trip_id // أو البث لخط لا يملكه. main_driver_id هو نفسه sub في JWT (الحساب في // جدول driver الرئيسي)، وليس transit_drivers.id. function transitSetTripOwner(int $tripId, string $mainDriverId, int $routeId): void { global $redisLocation; if (!$redisLocation || !$mainDriverId) return; $key = "transit:trip:{$tripId}:owner"; $redisLocation->hMSet($key, ['driver_id' => $mainDriverId, 'route_id' => $routeId]); $redisLocation->expire($key, 86400); } function transitClearTripOwner(int $tripId): void { global $redisLocation; if (!$redisLocation) return; $redisLocation->del("transit:trip:{$tripId}:owner"); } // ── days_mask helpers ──────────────────────────────────────── // bit0=Sun … bit6=Sat | 62 = 0b0111110 = الأحد–الخميس function transitDayActive(int $mask): bool { return (bool)(($mask >> (int)date('w')) & 1); }