Files
tripz-llc/backend/Admin/notifications/broadcast.php
T
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00

126 lines
4.7 KiB
PHP

<?php
/**
* Admin/notifications/broadcast.php
* إرسال إشعار جماعي إلى كل السائقين أو كل الركاب.
*
* لماذا نقطة وسيطة بدل استدعاء ride/firebase/send_fcm.php من الواجهة؟
* - send_fcm.php داخلية ومحمية بمفتاح سرّي (FCM_INTERNAL_API_KEY)، ولا يجوز
* أن يحمل المتصفح هذا المفتاح لأنه سيُكشف لأي مستخدم.
* - send_fcm.php لا تعرف من المُرسِل، فلا تستطيع تقييد الصلاحية ولا التدقيق.
*
* هذه النقطة تفرض JWT + بصمة الجهاز (عبر connect.php) ودور super_admin، ثم
* تُمرّر الطلب داخلياً مع المفتاح السرّي وتسجّل العملية في سجل التدقيق.
*/
require_once __DIR__ . '/../../connect.php';
// إشعار جماعي يصل كل مستخدمي المنصة فوراً ولا يمكن سحبه بعد الإرسال.
if ($role !== 'super_admin') {
http_response_code(403);
echo json_encode([
'status' => 'failure',
'message' => 'Forbidden. Super Admin access required to broadcast notifications.',
], JSON_UNESCAPED_UNICODE);
exit;
}
$audience = filterRequest('audience');
$title = filterRequest('title');
$body = filterRequest('body');
// المواضيع المسموح بها فقط — يشترك بها التطبيقان (siro_driver / siro_rider).
// قصرها على قائمة ثابتة يمنع استخدام النقطة لبثّ رسائل إلى مواضيع عشوائية
// أو إلى توكن جهاز بعينه.
$ALLOWED_AUDIENCES = [
'drivers' => 'drivers',
'passengers' => 'passengers',
];
if (!isset($ALLOWED_AUDIENCES[$audience])) {
jsonError('Invalid audience. Allowed: ' . implode(', ', array_keys($ALLOWED_AUDIENCES)), 400);
}
$title = trim((string) $title);
$body = trim((string) $body);
if ($title === '' || $body === '') {
jsonError('Both title and body are required.', 400);
}
if (mb_strlen($title) > 120) {
jsonError('Title is too long (max 120 characters).', 400);
}
if (mb_strlen($body) > 1000) {
jsonError('Body is too long (max 1000 characters).', 400);
}
$topic = $ALLOWED_AUDIENCES[$audience];
// سجل التدقيق قبل الإرسال: نريد أثراً حتى لو فشل النداء أو انقطع.
securityLog("Broadcast notification requested", [
'user_id' => $user_id ?? 'unknown',
'audience' => $audience,
'title' => $title,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
if (function_exists('logAudit')) {
try {
logAudit($con, (string) ($user_id ?? 'unknown'), 'إرسال إشعار جماعي', 'notification', $topic, [
'audience' => $audience,
'title' => $title,
'body' => $body,
]);
} catch (Throwable $e) {
error_log("[Broadcast] audit log failed: " . $e->getMessage());
}
}
// الاستدعاء الداخلي لخدمة FCM
// من داخل حاوية php لا يوجد خادم ويب على 127.0.0.1 — الويب في حاوية nginx
// منفصلة، وتُعرف داخل شبكة Compose باسم الخدمة. هذا كان سبب فشل كل إشعار.
$fcmUrl = getenv('FCM_INTERNAL_URL') ?: 'http://nginx/backend/ride/firebase/send_fcm.php';
$payload = json_encode([
'target' => $topic,
'title' => $title,
'body' => $body,
'isTopic' => true,
'data' => ['category' => 'admin_broadcast'],
], JSON_UNESCAPED_UNICODE);
$headers = ['Content-Type: application/json; charset=UTF-8'];
$internalKey = getenv('FCM_INTERNAL_API_KEY');
if (!empty($internalKey)) {
$headers[] = 'X-API-KEY: ' . $internalKey;
}
$ch = curl_init($fcmUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode >= 400) {
$reason = $curlErr ?: (is_string($response) ? substr($response, 0, 200) : 'no response');
error_log("[Broadcast] FCM call failed (HTTP $httpCode) via $fcmUrl: $reason");
jsonError("Notification service unreachable at $fcmUrl — $reason", 502);
}
$decoded = json_decode((string) $response, true);
jsonSuccess([
'audience' => $audience,
'topic' => $topic,
'title' => $title,
'sent_by' => $user_id ?? null,
'sent_at' => date('Y-m-d H:i:s'),
'fcm_status' => $decoded['status'] ?? 'unknown',
], 'Broadcast delivered to the notification service.');