Update: 2026-07-12 18:53:59

This commit is contained in:
Hamza-Ayed
2026-07-12 18:53:59 +03:00
parent 8cd4a4b57e
commit 653d73422f
54 changed files with 2078 additions and 436 deletions
@@ -7,24 +7,7 @@ if ($role !== 'admin' && $role !== 'super_admin') {
exit;
}
/**
* تطبيع رقم الهاتف ليتوافق مع التخزين في قاعدة البيانات
*/
function normalizePhone($phone) {
$clean = preg_replace('/\D+/', '', $phone);
// Syria: 099XXXXXXX or 9639XXXXXXX
if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1);
if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean;
if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean;
// Jordan: 079XXXXXXX or 9627XXXXXXX
if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1);
if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean;
if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean;
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1);
if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean;
return $clean;
}
// التطبيع عبر normalizePhone() الموحّدة في core/helpers.php (نفس المنطق سابقاً)
$phone = filterRequest('phone');
if (!$phone) {
+1 -18
View File
@@ -7,24 +7,7 @@ if ($role !== 'admin' && $role !== 'super_admin') {
exit;
}
/**
* تطبيع رقم الهاتف ليتوافق مع التخزين في قاعدة البيانات
*/
function normalizePhone($phone) {
$clean = preg_replace('/\D+/', '', $phone);
// Syria: 099XXXXXXX or 9639XXXXXXX
if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1);
if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean;
if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean;
// Jordan: 079XXXXXXX or 9627XXXXXXX
if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1);
if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean;
if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean;
// Egypt: 010XXXXXXXX or 2010XXXXXXXX
if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1);
if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean;
return $clean;
}
// التطبيع عبر normalizePhone() الموحّدة في core/helpers.php (نفس المنطق سابقاً)
// 1. تسجيل بداية الطلب
$phone = filterRequest("phone");
+75
View File
@@ -0,0 +1,75 @@
<?php
// Admin/transit/org/update.php — تعديل بيانات مؤسسة + إدارة حالة العقد
// POST: org_id, [contract_status], [city], [contact_email], [website], [trial_ends_at]
//
// عند التعليق (suspended) أو الإنهاء (terminated):
// يُبطل جميع جلسات مشرفي المؤسسة فوراً (Redis + MySQL)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
$orgId = filterRequest('org_id', 'int');
if (!$orgId) jsonError('org_id is required', 400);
$st = $transit_con->prepare("SELECT id, contract_status FROM transit_orgs WHERE id=? LIMIT 1");
$st->execute([$orgId]);
$org = $st->fetch();
if (!$org) jsonError('Organization not found', 404);
$updates = [];
$params = [];
// حقول يمكن تحديثها
if (($v = filterRequest('city')) !== null) { $updates[] = 'city=?'; $params[] = $v; }
if (($v = filterRequest('contact_email')) !== null) { $updates[] = 'contact_email=?'; $params[] = $v; }
if (($v = filterRequest('website')) !== null) { $updates[] = 'website=?'; $params[] = $v; }
if (($v = filterRequest('trial_ends_at')) !== null) { $updates[] = 'trial_ends_at=?'; $params[] = $v; }
$newStatus = null;
if (($v = filterRequest('contract_status')) !== null) {
$allowed = ['active', 'trial', 'suspended', 'terminated'];
if (!in_array($v, $allowed)) {
jsonError('Invalid contract_status. Allowed: ' . implode(', ', $allowed), 400);
}
$newStatus = $v;
$updates[] = 'contract_status=?';
$params[] = $v;
}
if (empty($updates)) jsonError('No fields to update', 400);
$updates[] = 'updated_at=NOW()';
$params[] = $orgId;
$transit_con->prepare(
"UPDATE transit_orgs SET " . implode(', ', $updates) . " WHERE id=?"
)->execute($params);
// إبطال جلسات المشرفين عند التعليق أو الإنهاء
if ($newStatus && in_array($newStatus, ['suspended', 'terminated'])) {
$admins = $transit_con->prepare("SELECT id FROM transit_org_admins WHERE org_id=?");
$admins->execute([$orgId]);
foreach ($admins->fetchAll(PDO::FETCH_COLUMN) as $adminId) {
$sessions = $transit_con->prepare("SELECT token_hash FROM transit_sessions WHERE admin_id=?");
$sessions->execute([$adminId]);
foreach ($sessions->fetchAll(PDO::FETCH_COLUMN) as $hash) {
if ($redis) $redis->del("transit:session:{$hash}");
}
$transit_con->prepare("DELETE FROM transit_sessions WHERE admin_id=?")->execute([$adminId]);
}
appLog("[ADMIN][TRANSIT][ORG][update] org={$orgId} contract_status={$newStatus} — all admin sessions invalidated");
} else {
appLog("[ADMIN][TRANSIT][ORG][update] org={$orgId} updated=" . implode(',', $updates));
}
jsonSuccess(['org_id' => $orgId, 'contract_status' => $newStatus ?? $org['contract_status']]);
+58
View File
@@ -0,0 +1,58 @@
<?php
// Admin/transit/route/approve.php — فريق سيرو يعتمد أو يوقف خطاً
// POST: route_id, action (approve|suspend|reject)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
$routeId = filterRequest('route_id', 'int');
$action = filterRequest('action');
if (!$routeId) jsonError('route_id is required', 400);
$allowed = ['approve', 'suspend', 'reject'];
if (!in_array($action, $allowed)) jsonError('Invalid action. Allowed: ' . implode(', ', $allowed), 400);
$st = $transit_con->prepare("SELECT id, org_id, name_ar, status FROM transit_routes WHERE id=? LIMIT 1");
$st->execute([$routeId]);
$route = $st->fetch();
if (!$route) jsonError('Route not found', 404);
$statusMap = [
'approve' => 'active',
'suspend' => 'suspended',
'reject' => 'rejected',
];
$newStatus = $statusMap[$action];
if ($route['status'] === $newStatus) {
jsonError("Route is already in status: {$newStatus}", 409);
}
$transit_con->prepare(
"UPDATE transit_routes
SET status=?, approved_by=?, approved_at=NOW(), updated_at=NOW()
WHERE id=?"
)->execute([$newStatus, (string)$user_id, $routeId]);
appLog("[TRANSIT][ROUTE] route #{$routeId} org#{$route['org_id']} → {$newStatus} by admin #{$user_id}", 'INFO');
// كتابة في Redis للسوكيت: transit:route_org:{routeId} → org_id
// يُستخدم في passenger_socket لتحقق العضوية
if (isset($redisLocation) && $redisLocation) {
$redisLocation->set("transit:route_org:{$routeId}", (string)$route['org_id']);
}
jsonSuccess([
'route_id' => $routeId,
'route_name' => $route['name_ar'],
'new_status' => $newStatus,
], "Route {$action}d successfully");
+48
View File
@@ -0,0 +1,48 @@
<?php
// Admin/transit/route/pending.php — قائمة الخطوط المسودة بانتظار الاعتماد
// POST: — (اختياري: org_id للفلترة)
require_once __DIR__ . '/../../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
jsonError('Unauthorized: Admin access required', 403);
}
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
require_once __DIR__ . '/../../../transit/functions.php';
$orgIdFilter = filterRequest('org_id', 'int');
$sql = "SELECT r.id, r.org_id, r.name_ar, r.name_en, r.direction, r.distance_km,
r.duration_min, r.status, r.created_at,
o.name_ar AS org_name, o.type AS org_type, o.country,
(SELECT COUNT(*) FROM transit_stops s WHERE s.route_id = r.id) AS stops_count
FROM transit_routes r
JOIN transit_orgs o ON o.id = r.org_id
WHERE r.status = 'draft'";
$params = [];
if ($orgIdFilter) {
$sql .= " AND r.org_id = ?";
$params[] = $orgIdFilter;
}
$sql .= " ORDER BY r.created_at DESC LIMIT 100";
$st = $transit_con->prepare($sql);
$st->execute($params);
$routes = $st->fetchAll();
// جلب محطات كل خط للمعاينة
$stStops = $transit_con->prepare(
"SELECT id, sequence, name_ar, latitude, longitude, is_major
FROM transit_stops WHERE route_id=? ORDER BY sequence ASC"
);
foreach ($routes as &$r) {
$stStops->execute([$r['id']]);
$r['stops'] = $stStops->fetchAll();
}
unset($r);
jsonSuccess(['routes' => $routes, 'total' => count($routes)]);
-40
View File
@@ -1,40 +0,0 @@
import os
# Configuration
PROJECT_DIR = '.'
OUTPUT_FILE = 'siro_v1_secure_latest.md'
EXCLUDED_DIRS = {'.git', 'vendor', 'node_modules', '.gemini'}
EXCLUDED_FILES = {OUTPUT_FILE, 'aggregate_files.py'}
def aggregate_files():
with open(OUTPUT_FILE, 'w', encoding='utf-8') as outfile:
outfile.write(f'# Siro V1 - Secure Latest Version\n\n')
for root, dirs, files in os.walk(PROJECT_DIR):
# Prune excluded directories
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
for file in files:
if file in EXCLUDED_FILES:
continue
filepath = os.path.join(root, file)
rel_path = os.path.relpath(filepath, PROJECT_DIR)
# We mainly want to include code files
if any(file.endswith(ext) for ext in ['.php', '.sql', '.ini', '.json', '.md', '.txt', '.py', '.sh']):
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as infile:
content = infile.read()
outfile.write(f'## File: {rel_path}\n')
outfile.write(f'```\n')
outfile.write(content)
outfile.write(f'\n```\n\n')
print(f"Added: {rel_path}")
except Exception as e:
print(f"Could not read {rel_path}: {e}")
if __name__ == "__main__":
aggregate_files()
print(f"\nDone! File created: {OUTPUT_FILE}")
+1 -17
View File
@@ -51,23 +51,7 @@ if (empty($rawPhone)) {
exit;
}
// تطبيع رقم الهاتف حسب الدولة (بدون +، بدون أصفار زائدة)
// حتى يتطابق مع التخزين في قاعدة البيانات (مثال: 9639XXXXXXX)
function normalizePhone($phone) {
$clean = preg_replace('/\D+/', '', $phone);
// Syria: 099XXXXXXX → 9639XXXXXXX
if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1);
if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean;
if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean;
// Jordan: 079XXXXXXX → 9627XXXXXXX
if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1);
if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean;
if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean;
// Egypt: 010XXXXXXXX → 2010XXXXXXXX
if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1);
if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean;
return $clean;
}
// التطبيع عبر normalizePhone() الموحّدة في core/helpers.php (نفس المنطق سابقاً)
$phone = normalizePhone($rawPhone);
try {
+10 -3
View File
@@ -5,6 +5,10 @@
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
// Rate limiting — حد صارم على طلبات OTP
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
@@ -13,7 +17,8 @@ if (!$rawPhone) jsonError('Phone is required');
$phone = normalizePhone($rawPhone);
if (transitIsOtpLocked($phone)) {
jsonError('Too many attempts. Try again in 30 minutes.', 429);
// نفس رسالة النجاح — لا نكشف أن الحساب مقفل
jsonSuccess(null, 'OTP sent successfully');
}
$phoneEnc = $encryptionHelper->encryptData($phone);
@@ -26,8 +31,10 @@ $st = $transit_con->prepare(
$st->execute([$phoneEnc]);
$admin = $st->fetch();
if (!$admin) jsonError('Invalid credentials', 401);
if ($admin['contract_status'] === 'terminated') jsonError('Account suspended', 403);
// لا نكشف إن كان الهاتف موجوداً أم لا — نفس الرد دائماً
if (!$admin || $admin['contract_status'] === 'terminated') {
jsonSuccess(null, 'OTP sent successfully');
}
transitSendAdminOtp($phone);
+4
View File
@@ -5,6 +5,10 @@
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
// Rate limiting
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
+34
View File
@@ -0,0 +1,34 @@
<?php
// transit/admin/logout.php — تسجيل خروج مشرف المؤسسة (حذف الجلسة)
// POST: — (التوكن في الهيدر Authorization: Bearer)
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { jsonError('Transit service unavailable', 503); }
// استخرج التوكن من الهيدر
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$token = '';
if (preg_match('/Bearer\s+(\S+)/i', $authHeader, $m)) {
$token = $m[1];
} else {
$token = filterRequest('token') ?? '';
}
if (!$token) jsonError('No session token provided', 400);
$hash = hash('sha256', $token);
// حذف من Redis
if ($redis) $redis->del("siro:transit:session:{$hash}");
// حذف من MySQL
try {
$transit_con->prepare("DELETE FROM transit_sessions WHERE token_hash=?")->execute([$hash]);
} catch (Throwable $e) {
// لا بأس — الجلسة انتهت بالـ Redis
}
jsonSuccess(null, 'Logged out successfully');
+4
View File
@@ -18,6 +18,10 @@ if (in_array($origin, $adminOrigins)) {
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
// Rate limiting على جلسات لوحة المشرف
$limiter = new RateLimiter($redis);
$limiter->enforce(RateLimiter::identifier(), 'api');
// اتصال Transit DB
try {
$transit_con = Database::get('transit');
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
* transit/cron_sync_members.php
* ─────────────────────────────────────────────────────────────────────
* يعيد بناء مجموعات transit:org_members:{orgId} في Redis (location server)
* من قاعدة البيانات MySQL — للتشغيل اليدوي أو الكرون اليومي.
*
* يُستخدَم بعد:
* • هجرة قاعدة البيانات (ALTER/INSERT جماعي)
* • انقطاع Redis (مسح البيانات بعد إعادة التشغيل)
* • مزامنة دورية للتأكد من عدم الانزلاق
*
* crontab مقترح:
* 0 3 * * * php /home/intaleq-rides/backend/transit/cron_sync_members.php >> /var/log/transit_sync_members.log 2>&1
*/
require_once __DIR__ . '/../core/bootstrap.php';
echo "[" . date('Y-m-d H:i:s') . "] Starting transit:org_members sync...\n";
if (!$redisLocation) {
echo "[ERROR] \$redisLocation is not available. Check REDIS_LOCATION_HOST in .env\n";
exit(1);
}
try {
$transit_con = Database::get('transit');
} catch (Exception $e) {
echo "[ERROR] Cannot connect to siroTransitDb: " . $e->getMessage() . "\n";
exit(1);
}
// جلب كل العضويات النشطة مع org_id
$st = $transit_con->prepare(
"SELECT org_id, passenger_id
FROM transit_enrollments
WHERE status = 'active'
AND passenger_id IS NOT NULL"
);
$st->execute();
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
echo "[INFO] Found " . count($rows) . " active enrollments to sync\n";
// تجميع per org
$byOrg = [];
foreach ($rows as $row) {
$byOrg[(int)$row['org_id']][] = (string)$row['passenger_id'];
}
$synced = 0;
foreach ($byOrg as $orgId => $passengerIds) {
$key = "transit:org_members:{$orgId}";
// بناء كامل: احذف المجموعة القديمة وأعد بناءها بدفعة واحدة
$redisLocation->del($key);
if (!empty($passengerIds)) {
// sAdd يقبل array ابتداءً من phpredis 5.x
$redisLocation->sAdd($key, ...$passengerIds);
$redisLocation->expire($key, 7 * 86400);
}
echo "[SYNC] org_id={$orgId} → {$key} ({" . count($passengerIds) . "} members)\n";
$synced++;
}
echo "[" . date('Y-m-d H:i:s') . "] Done. Synced {$synced} orgs, " . count($rows) . " total enrollments.\n";
@@ -0,0 +1,61 @@
<?php
// transit/driver/activate_by_app.php — السائق يفعّل حسابه بتوكن الدعوة (عبر JWT)
// POST: invite_token
// المصادقة: JWT السائق الرئيسي من connect_app.php
require_once __DIR__ . '/../../transit/connect_app.php';
if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403);
$inviteToken = filterRequest('invite_token');
if (!$inviteToken) jsonError('invite_token is required', 400);
// البحث بالتوكن فقط — لا نثق بأي معرّف آخر من العميل
$st = $transit_con->prepare(
"SELECT d.id, d.org_id, d.name, d.status, o.name_ar AS org_name
FROM transit_drivers d
JOIN transit_orgs o ON o.id = d.org_id
WHERE d.invite_token=? LIMIT 1"
);
$st->execute([$inviteToken]);
$driver = $st->fetch();
if (!$driver) jsonError('Invalid invite token', 404);
if ($driver['status'] === 'suspended') jsonError('Account suspended', 403);
// تحقق أن main_driver_id ليس مضبوطاً لحساب آخر
if ($driver['status'] === 'active') {
$chk = $transit_con->prepare(
"SELECT main_driver_id FROM transit_drivers WHERE id=? LIMIT 1"
);
$chk->execute([$driver['id']]);
$row = $chk->fetch();
if ($row && $row['main_driver_id'] && (string)$row['main_driver_id'] !== (string)$transit_user_id) {
jsonError('This invite token is already linked to another account', 409);
}
// إذا كان نفس الحساب — رد نجاح مباشرة
if ($row && (string)$row['main_driver_id'] === (string)$transit_user_id) {
jsonSuccess([
'driver_id' => (int)$driver['id'],
'org_id' => (int)$driver['org_id'],
'org_name' => $driver['org_name'],
'name' => $driver['name'],
], 'Already activated');
}
}
// ضبط main_driver_id وتفعيل الحساب
$transit_con->prepare(
"UPDATE transit_drivers
SET main_driver_id=?, status='active', activated_at=NOW(), invite_token=NULL, updated_at=NOW()
WHERE id=?"
)->execute([(string)$transit_user_id, $driver['id']]);
appLog("[TRANSIT][DRIVER] activated driver #{$driver['id']} (org#{$driver['org_id']}) linked to main_driver_id={$transit_user_id}", 'INFO');
jsonSuccess([
'driver_id' => (int)$driver['id'],
'org_id' => (int)$driver['org_id'],
'org_name' => $driver['org_name'],
'name' => $driver['name'],
], 'Account activated successfully. Welcome to مواصلاتي!');
+1 -1
View File
@@ -7,7 +7,7 @@ $status = filterRequest('status') ?: 'active';
$allowed = ['all','invited','active','suspended'];
if (!in_array($status, $allowed)) $status = 'active';
$sql = "SELECT id, name, status, activated_at, created_at FROM transit_drivers WHERE org_id=?";
$sql = "SELECT id, name, phone, status, activated_at, created_at FROM transit_drivers WHERE org_id=?";
$params = [$transit_org_id];
if ($status !== 'all') { $sql .= " AND status=?"; $params[] = $status; }
$sql .= " ORDER BY name ASC";
+2
View File
@@ -44,6 +44,8 @@ if ($rosterRow) {
WHERE id=?"
)->execute([$transit_user_id, $rosterRow['id']]);
transitCacheEnrollment($orgId, (string)$transit_user_id);
jsonSuccess([
'enrollment_id' => (int)$rosterRow['id'],
'org_name' => $org['name_ar'],
+12 -7
View File
@@ -27,13 +27,18 @@ $transit_con->prepare(
WHERE id=?"
)->execute([$newStatus, $expiresAt, $enrollId]);
if ($action === 'approve' && $enroll['passenger_id']) {
transitSendTopicNotification(
'passenger_' . $enroll['passenger_id'],
'تم تفعيل عضويتك',
'يمكنك الآن متابعة باصات جامعتك عبر مواصلاتي',
['type' => 'transit_enrollment_approved']
);
if ($enroll['passenger_id']) {
if ($action === 'approve') {
transitCacheEnrollment($transit_org_id, (string)$enroll['passenger_id']);
transitSendTopicNotification(
'passenger_' . $enroll['passenger_id'],
'تم تفعيل عضويتك',
'يمكنك الآن متابعة باصات جامعتك عبر مواصلاتي',
['type' => 'transit_enrollment_approved']
);
} else {
transitDropEnrollment($transit_org_id, (string)$enroll['passenger_id']);
}
}
jsonSuccess(['enrollment_id' => $enrollId, 'status' => $newStatus]);
+23
View File
@@ -196,6 +196,29 @@ function transitClearTripOwner(int $tripId): void
$redisLocation->del("transit:trip:{$tripId}:owner");
}
// ── عضوية الراكب في Redis (location Redis — بدون بادئة siro:) ──────
// يستخدمها passenger_socket.php للتحقق قبل قبول subscribe_transit_route.
// تُكتب عند: تفعيل عضوية برقم الكشف، أو موافقة المشرف على الطلب اليدوي.
// تُحذف عند: رفض أو تعليق العضوية.
function transitCacheEnrollment(int $orgId, string $passengerId): void
{
global $redisLocation;
if (!$redisLocation || !$passengerId) return;
$key = "transit:org_members:{$orgId}";
$redisLocation->sAdd($key, $passengerId);
$redisLocation->expire($key, 7 * 86400); // تجديد TTL سبعة أيام
}
function transitDropEnrollment(int $orgId, string $passengerId): void
{
global $redisLocation;
if (!$redisLocation || !$passengerId) return;
$redisLocation->sRem("transit:org_members:{$orgId}", $passengerId);
}
// ── days_mask helpers ────────────────────────────────────────
// bit0=Sun … bit6=Sat | 62 = 0b0111110 = الأحد–الخميس
+1 -1
View File
@@ -5,7 +5,7 @@
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
$internalKey = getenv('SOCKET_INTERNAL_KEY') ?: '';
$internalKey = getInternalSocketKey(); // نفس مفتاح النظام الموحّد INTERNAL_SOCKET_KEY
$requestKey = $_SERVER['HTTP_X_INTERNAL_KEY'] ?? filterRequest('internal_key') ?? '';
if (!$internalKey || !hash_equals($internalKey, $requestKey)) jsonError('Forbidden', 403);
+3 -3
View File
@@ -31,8 +31,8 @@ if ($driverId) {
$transit_con->prepare(
"INSERT INTO transit_schedules
(route_id, org_id, vehicle_id, driver_id, days_mask, departure_time, valid_from, valid_until, created_by)
VALUES (?,?,?,?,?,?,?,?,?)"
)->execute([$routeId, $transit_org_id, $vehicleId, $driverId, $daysMask, $departureTime, $validFrom, $validUntil, $transit_admin_id]);
(route_id, org_id, vehicle_id, driver_id, days_mask, departure_time, valid_from, valid_until)
VALUES (?,?,?,?,?,?,?,?)"
)->execute([$routeId, $transit_org_id, $vehicleId, $driverId, $daysMask, $departureTime, $validFrom, $validUntil]);
jsonSuccess(['schedule_id' => (int)$transit_con->lastInsertId()], 'Schedule added');
+10 -2
View File
@@ -198,7 +198,7 @@ CREATE TABLE IF NOT EXISTS `transit_trips` (
`route_id` INT UNSIGNED NOT NULL,
`org_id` INT UNSIGNED NOT NULL,
`driver_id` INT UNSIGNED NOT NULL,
`vehicle_id` INT UNSIGNED NOT NULL,
`vehicle_id` INT UNSIGNED DEFAULT NULL COMMENT 'NULL إن كان الجدول بلا باص مخصص',
`trip_date` DATE NOT NULL,
`status` ENUM('scheduled','started','completed','cancelled','no_show') NOT NULL DEFAULT 'scheduled',
`started_at` TIMESTAMP DEFAULT NULL,
@@ -221,7 +221,7 @@ CREATE TABLE IF NOT EXISTS `transit_trips` (
CREATE TABLE IF NOT EXISTS `transit_enrollments` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`org_id` INT UNSIGNED NOT NULL,
`passenger_id` VARCHAR(100) NOT NULL COMMENT 'معرّف في جدول passengers الرئيسي',
`passenger_id` VARCHAR(100) DEFAULT NULL COMMENT 'معرّف في جدول passengers الرئيسي — NULL لصفوف الكشف قبل تفعيل الطالب',
`student_id` VARCHAR(50) NOT NULL COMMENT 'الرقم الجامعي/الوظيفي (encrypted)',
`member_name` VARCHAR(120) DEFAULT NULL COMMENT 'الاسم من كشف الجامعة',
`preferred_stop_id` INT UNSIGNED DEFAULT NULL COMMENT 'محطتي المفضلة',
@@ -331,6 +331,14 @@ CREATE TABLE IF NOT EXISTS `transit_otp_log` (
SET FOREIGN_KEY_CHECKS = 1;
-- -----------------------------------------------------------------
-- ترحيل (Migration) لقاعدة أُنشئت قبل 2026-07-12 — نفّذ مرة واحدة:
-- ALTER TABLE transit_enrollments MODIFY passenger_id VARCHAR(100) DEFAULT NULL
-- COMMENT 'معرّف في جدول passengers الرئيسي — NULL لصفوف الكشف قبل تفعيل الطالب';
-- ALTER TABLE transit_trips MODIFY vehicle_id INT UNSIGNED DEFAULT NULL
-- COMMENT 'NULL إن كان الجدول بلا باص مخصص';
-- -----------------------------------------------------------------
-- -----------------------------------------------------------------
-- ملاحظات الـ Redis (keys تُكتب من PHP — ليست في SQL)
-- transit:trip:{trip_id}:pos → آخر موقع GPS للباص (Hash: lat,lng,ts)
+14 -5
View File
@@ -3,12 +3,21 @@
require_once __DIR__ . '/../../transit/connect_app.php';
requireTransitFields(['trip_id', 'driver_transit_id', 'delay_minutes']);
if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403);
$tripId = filterRequest('trip_id', 'int');
$driverId = filterRequest('driver_transit_id', 'int');
$delay = min((int)filterRequest('delay_minutes'), 120);
$reason = filterRequest('reason');
$tripId = filterRequest('trip_id', 'int');
$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();
if (!$driverRec) jsonError('Driver not found or not active', 403);
$driverId = (int)$driverRec['id'];
$st = $transit_con->prepare(
"SELECT t.id, t.route_id, r.name_ar AS route_name
+12 -3
View File
@@ -3,10 +3,19 @@
require_once __DIR__ . '/../../transit/connect_app.php';
requireTransitFields(['trip_id', 'driver_transit_id']);
if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403);
$tripId = filterRequest('trip_id', 'int');
$driverId = filterRequest('driver_transit_id', 'int');
$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();
if (!$driverRec) jsonError('Driver not found or not active', 403);
$driverId = (int)$driverRec['id'];
$st = $transit_con->prepare(
"SELECT id, route_id, status FROM transit_trips WHERE id=? AND driver_id=? LIMIT 1"
+1 -1
View File
@@ -19,7 +19,7 @@ if (!$tripId) jsonError('No active trip found', 404);
$st = $transit_con->prepare(
"SELECT t.id, t.org_id, t.status, t.delay_minutes, t.current_stop_seq, t.started_at,
r.name_ar AS route_name, sc.departure_time, d.name AS driver_name
r.name_ar AS route_name, r.polyline AS route_polyline, sc.departure_time, d.name AS driver_name
FROM transit_trips t
JOIN transit_routes r ON r.id = t.route_id
JOIN transit_schedules sc ON sc.id = t.schedule_id
+18 -10
View File
@@ -3,18 +3,26 @@
require_once __DIR__ . '/../../transit/connect_app.php';
requireTransitFields(['trip_id', 'driver_transit_id', 'lat', 'lng']);
if ($transit_user_role !== 'driver') jsonError('Only drivers can access this endpoint', 403);
$tripId = filterRequest('trip_id', 'int');
$driverId = filterRequest('driver_transit_id', 'int');
$lat = (float)filterRequest('lat');
$lng = (float)filterRequest('lng');
requireTransitFields(['trip_id', 'lat', 'lng']);
$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();
if (!$driverRec) jsonError('Driver not found or not active', 403);
$driverId = (int)$driverRec['id'];
$st = $transit_con->prepare(
"SELECT t.id, t.route_id, t.status, r.name_ar AS route_name, d.main_driver_id
"SELECT t.id, t.route_id, t.status, r.name_ar AS route_name
FROM transit_trips t
JOIN transit_routes r ON r.id = t.route_id
JOIN transit_drivers d ON d.id = t.driver_id
JOIN transit_routes r ON r.id = t.route_id
WHERE t.id=? AND t.driver_id=? LIMIT 1"
);
$st->execute([$tripId, $driverId]);
@@ -24,7 +32,6 @@ if (!$trip) jsonError('Trip not found', 404);
if ($trip['status'] === 'started') jsonError('Trip already started', 409);
if ($trip['status'] === 'completed') jsonError('Trip already completed', 409);
if ($trip['status'] === 'cancelled') jsonError('Trip was cancelled', 409);
if (!$trip['main_driver_id']) jsonError('Driver has no linked main account — cannot start live tracking', 422);
$transit_con->prepare(
"UPDATE transit_trips SET status='started', started_at=NOW(), current_stop_seq=1 WHERE id=?"
@@ -33,7 +40,8 @@ $transit_con->prepare(
transitUpdateBusPosition($tripId, $lat, $lng);
// تخزين ملكية الرحلة — يتحقق منها driver_socket قبل أي بثّ حي للراكبين
transitSetTripOwner($tripId, (string)$trip['main_driver_id'], (int)$trip['route_id']);
// transit_user_id هو main_driver_id بالفعل (يُستخرج من JWT السائق)
transitSetTripOwner($tripId, (string)$transit_user_id, (int)$trip['route_id']);
global $redis;
if ($redis) {
+6 -5
View File
@@ -3,15 +3,16 @@
require_once __DIR__ . '/../../transit/connect_app.php';
$driverTransitId = filterRequest('driver_transit_id', 'int');
if (!$driverTransitId) jsonError('driver_transit_id is required', 400);
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 id=? AND status='active' LIMIT 1"
"SELECT id, org_id FROM transit_drivers WHERE main_driver_id=? AND status='active' LIMIT 1"
);
$chk->execute([$driverTransitId]);
$chk->execute([(string)$transit_user_id]);
$driver = $chk->fetch();
if (!$driver) jsonError('Driver not found or not active', 403);
if (!$driver) jsonError('Driver not found or not active — activate your account first', 403);
$driverTransitId = (int)$driver['id'];
$today = date('Y-m-d');
-38
View File
@@ -1,38 +0,0 @@
<?php
// transit/trip/update_position.php
// ⚠️ مهجور (fallback فقط): المسار الأساسي لموقع الباص هو WebSocket:
// سائق الباص يبعث socket.emit('update_bus_location', {...}) إلى driver_socket:2020
// الذي يخزّن الموقع ويبثّه حياً لكل ركاب الخط عبر passenger_socket.
// هذا الـ endpoint يكتب الموقع في Redis فقط ولا يبثّه للركاب — لا تستخدمه
// إلا كخطة بديلة عند تعذّر السوكت. الموقع يُخزَّن على Redis سيرفر الموقع.
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try { $transit_con = Database::get('transit'); }
catch (Exception $e) { http_response_code(503); exit; }
$tripId = filterRequest('trip_id', 'int');
$driverId = filterRequest('driver_transit_id', 'int');
$lat = (float)filterRequest('lat');
$lng = (float)filterRequest('lng');
$stopSeq = filterRequest('current_stop_seq', 'int');
if (!$tripId || !$driverId || !$lat || !$lng) {
jsonError('Missing required fields', 400);
}
transitUpdateBusPosition($tripId, $lat, $lng);
if ($stopSeq !== null) {
$transit_con->prepare(
"UPDATE transit_trips SET current_stop_seq=?, updated_at=NOW() WHERE id=? AND driver_id=?"
)->execute([$stopSeq, $tripId, $driverId]);
global $redis;
if ($redis) {
$redis->publish("transit:trip:{$tripId}:stop", json_encode(['seq' => $stopSeq, 'ts' => time()]));
}
}
jsonSuccess(['ok' => true]);