Files
Siro/backend/transit/functions.php
T

206 lines
7.2 KiB
PHP

<?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);
}