Update: 2026-07-12 05:40:28
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// transit/functions.php — دوال خاصة بمنصة مواصلاتي فقط
|
||||
//
|
||||
// ما لا يوجد هنا (يُستخدم مباشرة من النظام الأصلي بعد bootstrap):
|
||||
// • filterRequest() ← core/helpers.php
|
||||
// • jsonSuccess() / jsonError() ← core/helpers.php
|
||||
// • appLog() / securityLog() ← core/helpers.php
|
||||
// • normalizePhone($phone) ← core/helpers.php — يطبّع الهاتف دائماً قبل التشفير أو الاستعلام
|
||||
// • OtpService($redis) ← core/Services/OtpService.php
|
||||
// • $encryptionHelper ← مهيَّأ في bootstrap.php
|
||||
// • $redis ← مهيَّأ في bootstrap.php
|
||||
// • sendWhatsAppFromServer() ← backend/functions.php
|
||||
// • FcmService($redis) ← core/Services/FcmService.php
|
||||
// ============================================================
|
||||
|
||||
// ── حقول مطلوبة (غلاف رفيع يستخدم filterRequest + jsonError) ──
|
||||
function requireTransitFields(array $fields): void
|
||||
{
|
||||
foreach ($fields as $f) {
|
||||
if (filterRequest($f) === null) {
|
||||
jsonError("Missing required field: $f", 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Session مشرف الويب (OTP-based — مستقلة عن JWT) ──────────
|
||||
|
||||
function transitCreateSession(int $adminId, int $orgId): string
|
||||
{
|
||||
global $redis;
|
||||
|
||||
$token = bin2hex(random_bytes(32)); // 64 hex chars
|
||||
$hash = hash('sha256', $token);
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + 86400);
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
$con = Database::get('transit');
|
||||
$con->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);
|
||||
}
|
||||
Reference in New Issue
Block a user