206 lines
7.2 KiB
PHP
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);
|
|
}
|