Commit Graph
380 Commits
Author SHA1 Message Date
Hamza-Ayed 04c214bdf6 Update: 2026-08-03 15:22:47 2026-08-03 15:22:47 +03:00
Hamza-Ayed dbf1e7870f Update: 2026-08-03 13:37:04 2026-08-03 13:37:04 +03:00
Hamza-Ayed 79333a4bee Update: 2026-08-03 13:22:27 2026-08-03 13:22:27 +03:00
Hamza-Ayed b60129d246 Update: 2026-08-03 13:10:56 2026-08-03 13:10:57 +03:00
Hamza-Ayed f36b30f0b8 Update: 2026-08-03 12:30:24 2026-08-03 12:30:25 +03:00
Hamza-Ayed ec467b01c6 Update: 2026-08-03 11:52:03 2026-08-03 11:52:03 +03:00
Hamza-Ayed 0334f9881f Update: 2026-08-03 11:32:12 2026-08-03 11:32:12 +03:00
Hamza-Ayed 2b9f696372 Update: 2026-08-03 00:20:40 2026-08-03 00:20:40 +03:00
Hamza-Ayed 2c962bf80a Update: 2026-08-02 22:51:18 2026-08-02 22:51:18 +03:00
Hamza-Ayed 02e652a786 Update: 2026-08-02 18:28:40 2026-08-02 18:28:40 +03:00
Hamza-Ayed 4620e84d34 Update: 2026-08-02 17:52:28 2026-08-02 17:52:28 +03:00
Hamza-Ayed 915d517ba7 Port all fixes from IntaleqApp (Wallet, CLIQ, OTP, Docker) 2026-08-01 03:58:37 +03:00
Hamza-Ayed 410fb14d77 Update: 2026-07-31 19:21:38 2026-07-31 19:21:38 +03:00
Hamza-Ayed cf3fea3834 Update: 2026-07-31 19:18:42 2026-07-31 19:18:43 +03:00
Hamza-Ayed 7a576b7327 Update: 2026-07-30 12:31:26 2026-07-30 12:31:27 +03:00
Hamza-AyedandClaude Opus 5 b84e26fe9a تسجيل نتيجة send_fcm.php — مسار الرسائل كان صندوقاً أسود بالكامل
send_fcm.php هي نقطة كل الرسائل والمكالمات بين الراكب والسائق (التطبيقان
يستدعيانها مباشرة)، ولم تكن تحتوي أي error_log إطلاقاً — بخلاف FcmService
التي تطبع [FCM_DEBUG] وتخدم دورة حياة الرحلة فقط.

النتيجة أن فشل الرسائل كان غير قابل للتشخيص: لا سبب، ولا رد Google، ولا
حتى معرفة إن كان الطلب وصل. وقد وعدت المستخدم بقراءة [FCM_DEBUG] لهذا
المسار وهو وعد خاطئ — لا وجود له هنا.

أُضيفت ثلاث نقاط:
- نتيجة الإرسال: category + طرف من التوكن + http + طول التوكن + رد Google
  عند الفشل. طول التوكن مقصود: توكن FCM الحقيقي ~163 محرفاً، والمشفّر في
  جدول tokens 216 — فيُكشف أي blob مشفّر من سطر واحد.
- رفض 403: التطبيقان لا يرسلان x-api-key، فلحظة ضبط FCM_INTERNAL_API_KEY
  تموت كل الرسائل بينهما بصمت. الآن يُسجَّل السبب صريحاً.
- رفض 400 على target فارغ: يعني أن المُرسِل لا يملك توكن الطرف الآخر
  (tokenPassenger أو driverToken لم يصله في حمولة القبول).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:35:01 +03:00
Hamza-AyedandClaude Opus 5 ef1240b130 تصحيح مفتاح Redis لموقع السائق: driver:location ← driver:public
الموضعان كانا يقرآن مفتاحاً لا يُكتب في أي مكان في المشروع:
  $redisLocation->hGetAll("driver:location:$driverId")

الكاتب الفعلي هو معالج الدفعات في loction_server/driver_socket.php، وهو
يكتب hmset على driver:profile:{id} و driver:public:{id} معاً بنفس الحقول
(lat/lng/heading/speed/status/updated_at). و driver:public له TTL 86400
بينما driver:profile له 900 فقط، فالعام هو الأنسب للقراءة.

الأثر: كانت حمولة القبول تصل الراكب بلا إحداثيات أولية للسائق، فلا يظهر
الماركر إلا بعد أول تحديث موقع من السوكيت أو الـ polling.

أحد الموضعين ملف getRideOrderID.php الذي أضفته في f66db7db — نسخت النمط
من acceptRide.php فنقلت الخطأ معه.

مثبَّت على الإنتاج: redis-cli --scan --pattern 'driver:*' أرجع
driver:public:<id> فقط، ولا شيء باسم driver:location.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:14:26 +03:00
Hamza-AyedandClaude Opus 5 f66db7db42 إضافة getRideOrderID.php المفقود — المسار الاحتياطي لبيانات السائق
تطبيق الراكب يطلب /ride/rides/getRideOrderID.php منذ البداية، والموجود على
السيرفر getRideOrderIDNew.php فقط ⇒ 404 صامت في getUpdatedRideForDriverApply.

هذا يفسّر عرضين ظنّاهما منفصلين:
- الـ polling يسبق الـ FCM فيضبط _isAcceptanceProcessed=true ثم يأخذ 404،
  فيُرفض بعدها payload الـ FCM الذي يحمل driver_info كاملاً
  ("Already processed") ⇒ الرحلة مقبولة بلا معلومات سائق.
- ولأن driverToken يبقى فارغاً، ترد send_fcm.php بـ 400 Missing: target
  ⇒ رسائل الراكب للسائق لا تُرسَل إطلاقاً. (الاتجاه المعاكس كان يعمل بعد
  إصلاح getRideWaiting.php — نفس خطأ التشفير معكوساً.)

getRideOrderIDNew.php لا يصلح بديلاً: داخلي عبر get_connect.php →
validateInternalKey فلا يستطيع التطبيق مناداته، ولا يرجع
ratingCount/completedRides/driverTier.

النقطة الجديدة:
- connect.php (JWT)، والراكب من الـ JWT فقط لا من الطلب (حماية IDOR).
- نفس استعلام acceptRide.php وشكل رده حرفياً حتى يقرأه
  _fillDriverDataLocally بنفس المفاتيح.
- فك تشفير driverToken.token — بلا ذلك يصل التطبيق blob يستخدمه كـ FCM
  target فيرفضه FCM بـ 400.
- تفادي تصادم المفاتيح: getUpdatedRideForDriverApply يقرأ
  passengerName + last_name كاسم الراكب، فلقب السائق نُقل إلى
  driver_last_name واسم الراكب الحقيقي يُرجَّع في مكانه.
- ride DB هو المرجع مع fallback على primary، ورحلة بلا سائق ترد success
  لا failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:18:02 +03:00
Hamza-AyedandClaude Opus 5 143146c1b4 إصلاح سوكيت الراكب وتوحيد مسارات الإشعارات
سوكيت الراكب (سبب عدم ظهور معلومات السائق عند القبول):
- تطبيق الراكب كان يرسل id فقط بلا jwt، و passenger_socket.php يرفض أي
  اتصال بلا jwt ⇒ الراكب لا ينضم لغرفته أبداً ولا يستلم ride_status_change
  ولا driver_location_update. تظهر حالة القبول عبر الـ polling فقط بينما
  driver_info يصل بالسوكيت وحده. (سوكيت السائق يعتبر الـ jwt اختيارياً،
  ومن هنا جاء التباين بين التطبيقين.)
- cancelled_by_driver كان يسقط من switch حالات الراكب فيبقى معلّقاً بعد
  إلغاء السائق.
- حماية socket (late) من القراءة قبل التهيئة عند الانسحاب بلا jwt.

الإشعارات والرسائل (سبب "مرات توصل ومرات لا"):
- جدول tokens يخزّن توكن الراكب مشفّراً، و getRideWaiting.php كان يرجعه
  بلا فك تشفير ⇒ من يقبل من قائمة السوق يحمل blob مشفّراً يستخدمه كـ FCM
  target فيرفضه FCM بـ 400: لا إشعار قبول ولا رسائل. ومن يقبل من الـ
  dispatch/FCM يحمل نصاً صريحاً فتعمل. الفرق كان في طريقة القبول.
- acceptRide.php يحلّ التوكن من القاعدة دائماً ولا يثق بالعميل (أصحّ أمنياً).
- market_new_ride كان لا يحمل passengerId ولا الإحداثيات فتصل "null"؛
  أُضيفت بلا أي PII لأن الحمولة تُبَثّ لكل سائق قريب لا للفائز فقط.
- send_fcm.php: مهلة على OAuth (كان يعلّق حتى مهلة PHP فتُسقط الرسالة
  بصمت)، توحيد ding→default لأندرويد، وحقن title/body/tone في data
  مطابقةً لـ FcmService.
- تطبيق السائق يقرأ title/body من data أولاً مثل الراكب، ولا يعرض فقاعة
  فارغة للرسائل الصامتة.

السوكيت والإعدادات:
- forwardLocationToPassengerSocket كان يقرأ lat/lng والحمولة فيها
  latitude/longitude ⇒ المسافة تخرج ضخمة والـ throttle معطّل تماماً
  فيُعاد التوجيه مع كل نبضة GPS.
- notifyPassengerOnRideServer كان يرجع null بصمت مطلق عند حجب العنوان.
- العنوان الافتراضي لسيرفر الموقع كان nginx/loction_server/driver_socket.php
  وهو ديمون Workerman لا يُخدَم عبر nginx ⇒ صار socket_driver:2021.
  (LOCATION_API_URL بقي على nginx لأن api_get_nearby.php سكربت عادي.)
- ride_server/passenger_socket.php (النسخة التي يشغّلها Docker) كانت ناقصة
  كل كود مواصلاتي الموجود في passenger_server/ ⇒ نُقل مع REDIS_HOST.
- .env.example: ALLOWED_SOCKET_URLS يغطّي أسماء حاويات Docker، وإضافة
  PASSENGER_SOCKET_INTERNAL_URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:47:23 +03:00
Hamza-Ayed f325ffce42 تصميم عالمي جديد، وإصلاح الخرائط ومراقب السيرفرات 2026-07-26 04:02:10 +03:00
Hamza-Ayed 2bacb1b9e1 تحديث شامل للوحة التحكم وإضافة كافة الميزات للـ WebSidebar 2026-07-26 03:22:47 +03:00
Hamza-Ayed 5ec54c03f5 تحديث شامل للوحة التحكم وإضافة كافة الميزات للـ WebSidebar 2026-07-26 02:58:59 +03:00
Hamza-Ayed a954f49307 Update: 2026-07-26 02:51:53 2026-07-26 02:51:54 +03:00
Hamza-Ayed 91fe0f78f7 Update: 2026-07-26 01:29:57 2026-07-26 01:29:57 +03:00
Hamza-Ayed 5f5b68a8cd Update: 2026-07-26 01:27:56 2026-07-26 01:27:56 +03:00
Hamza-Ayed 1664743ef9 Update: 2026-07-26 01:11:28 2026-07-26 01:11:28 +03:00
Hamza-Ayed 1dfc302a4f Update: 2026-07-26 01:03:40 2026-07-26 01:03:41 +03:00
Hamza-Ayed 76c8652bf0 Update: 2026-07-25 20:18:18 2026-07-25 20:18:18 +03:00
Hamza-Ayed e03b9c30d5 Update: 2026-07-25 18:43:44 2026-07-25 18:43:44 +03:00
Hamza-Ayed 20ea9aa12c Update: 2026-07-25 18:42:18 2026-07-25 18:42:18 +03:00
Hamza-Ayed 8d3e63d1c7 Update: 2026-07-25 18:40:38 2026-07-25 18:40:38 +03:00
Hamza-Ayed 4bbc687c15 Update: 2026-07-25 18:28:30 2026-07-25 18:28:31 +03:00
Hamza-AyedandClaude Opus 5 a0812dbd10 Fix broadcast delivery; make transit organisations manageable
Broadcasts never reached anyone. The internal FCM call defaulted to
127.0.0.1, which inside the php container is the php container itself — the
web server runs in a separate nginx container, reachable by service name on
the Compose network. Every send failed the curl and returned a generic 502.
The default now points at nginx, the URL is overridable via FCM_INTERNAL_URL,
and the error carries the actual reason and target instead of a bare status.

Transit organisations were a read-only count table with nothing to act on.
The module now supports the operations an admin actually needs:
- open an organisation for its counts, routes, recent trips and admins
- create one, including the founding administrator create.php requires
- edit city, contact details, contract status and trial end, with a
  confirmation when the contract changes since suspending cuts off service
- add an administrator, and enable or disable an existing one

Verified end to end against the endpoints' real payload shapes, including
that an incomplete create form is rejected before any request is sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 17:10:51 +03:00
Hamza-AyedandClaude Opus 5 bc1b0129e8 Fix v2 analytics status matching; give Growth and Analytics real charts
Eight queries across the v2 modules counted only status = 'Finished' and so
reported zero on live data, where the current ride pipeline writes
'completed': realtime revenue for today and yesterday, financial stats,
settlements, driver scorecard, driver ranking, and both revenue queries. All
now match either spelling.

Growth and Advanced Analytics rendered through the generic shape-detecting
renderer, which produced raw tables that said little. Both now have purpose-
built views:

- Growth: totals, 30-day joins, and a two-series daily chart. growth.php only
  returns days that had signups, so the series is expanded to a continuous
  30-day axis with explicit zeros — plotting the returned rows directly would
  hide the gaps and make a quiet month look like steady growth. A caption
  states how many days actually had a signup.
- Analytics: revenue summary tiles, a daily revenue trend, and the captain
  ranking, with a note explaining that platform share is what remains after
  the captain's cut.

Null aggregates render as "—" rather than 0.00, and markers are drawn only on
days with a value so a flat zero line stays readable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:55:58 +03:00
Hamza-AyedandClaude Opus 5 2135edcf43 Close the remaining ciphertext joins and a SQL injection in email verification
- Customer-service notes joined to the account by comparing encrypted phone
  columns. Both notes tables now carry phone_key, written when a note is
  saved, and the three joins match on it.
- The email_verifications join was comparing a plaintext column against an
  encrypted one, so it never matched and `verified` was always NULL in both
  passenger and driver sign-in. It is now resolved in PHP against the
  decrypted address, which fixes a pre-existing bug rather than only
  preparing for GCM.
- auth/sendVerifyEmail.php built all three of its statements by interpolating
  the request values into SQL. Any caller could inject through the email or
  token field. Now parameterised.
- serviceapp/register.php duplicate detection consults the users indexes and
  writes them with the row.

Sweep confirms no join or lookup compares two encrypted columns any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:48:10 +03:00
Hamza-AyedandClaude Opus 5 35a66935aa Repair verification joins broken by the OTP key change; extend backfill
Storing the verification phone as a keyed HMAC fixed OTP lookups but broke
every query that joined those tables back to the account, because
phone_verification*.phone_number no longer holds the same value as
driver.phone / passengers.phone. Six joins were affected, and four of them
feed the `verified` flag that the rider and driver apps check at sign-in — so
this was already failing under the current CBC mode, not only after a switch
to GCM.

Accounts now carry phone_key, computed exactly as otpPhoneKey() does, and the
joins match on it. It is written at registration for both apps and populated
for existing rows by the backfill.

The backfill also covers the columns added for the remaining lookups:
users.email_bidx/phone_bidx and driver.national_bidx, which were migrated but
never populated, and honours a per-field prefix so phone_key reproduces
otpPhoneKey's exact output.

Insert column/value counts verified with a paren-aware parser after editing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:40:53 +03:00
Hamza-AyedandClaude Opus 5 8d7e3118b5 Migrate remaining encrypted-column lookups to the blind index
Completes the set of queries that matched a freshly encrypted value against a
stored one, which only works while encryption is deterministic. Each keeps its
original comparison and adds an index comparison in the same WHERE, so nothing
changes today.

- passenger sign-in by email, service-staff sign-in, Firebase token lookup
- driver lookup by phone and by national number
- admin ride lookup and ride monitor (both tables)
- nabeh: driver status, user resolution, ride history, complaint submission

transit_org_admins lives in the transit database and has no index column, so
login there falls back to decrypting the small set of active admins and
comparing normalised numbers.

Schema: adds users.email_bidx/phone_bidx and driver.national_bidx with their
indexes.

Verified that every :*_bidx placeholder introduced is actually bound — an
unbound one is a fatal error at request time, not a silent miss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:36:32 +03:00
Hamza-AyedandClaude Opus 5 39b5a7fc7f Keep OTP phone numbers recoverable for customer-service follow-up
Storing the verification phone as a keyed HMAC made OTP lookups independent
of the encryption mode, but the hash is one-way — and customer service reads
those same rows to chase people who requested a code and never finished
registering. That workflow would have lost the number entirely.

The verification tables now carry both forms: phone_number holds the lookup
key, and a new phone_enc column holds the encrypted number, which is
decryptable when a human needs to call.

The two follow-up queries also compared the verification row against the
driver/passengers tables and the notes tables by matching ciphertext, which
only ever worked because encryption was deterministic. Under GCM every number
would have looked unregistered and every note would have disappeared. Both now
read the number from phone_enc and match on normalised plaintext, so they are
correct under either mode.

Rows written before phone_enc existed are skipped rather than shown without a
number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:27:29 +03:00
Hamza-AyedandClaude Opus 5 a1c19b052d Make OTP verification independent of the encryption mode
The verification tables (token_verification*, phone_verification*) use the
phone number as a lookup key: written when the code is sent, read when it is
checked. Storing it encrypted worked only because encryptData() is
deterministic — under AES-GCM the two sides would produce different
ciphertexts and no code would ever verify, locking every user out of
registration and OTP sign-in.

otpPhoneKey() stores a keyed HMAC of the normalised number instead. No schema
change is needed since the column is textual, local and international formats
now resolve to the same key, and the value cannot be reversed without the
pepper. It falls back to the previous behaviour when no pepper is configured.

Applied to both sides of every affected flow — request/verify, and the driver
and passenger send/verify pairs — including the OTP value itself where it is
compared by equality rather than decrypted. auth/otp/verify.php already
decrypts the token before comparing, so it needed no change there.

Also adds ENCRYPTION_MODE to EncryptionHelper: encryptData() writes GCM when
set to 'gcm', CBC otherwise. Verified in both directions — rows written under
CBC stay readable after switching, and rows written under GCM stay readable
after rolling back — so the switch is reversible by an environment variable.

The admin console's own OTP is unaffected: it keys the table by the stored
ciphertext read from adminUser, identical on both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:18:16 +03:00
Hamza-AyedandClaude Opus 5 c9b4d14da6 Route account lookups through the blind index and keep it fresh on write
These are the paths that must stop depending on deterministic encryption
before storage can move to AES-GCM. Each keeps its original ciphertext
comparison in the same statement, so behaviour is unchanged today and no
account becomes unreachable during the transition.

Lookups:
- auth/login.php — passenger sign-in matched the raw value against the
  encrypted column, which only works because encryptData() is CBC with a
  fixed IV.
- auth/passenger/register.php and auth/driver/register.php — duplicate
  detection. Without the index these would stop detecting existing accounts
  under GCM and allow the same phone to register twice.

Writes now populate the index in the same statement as the value:
- both registration paths write phone/email/name indexes with the row;
  driver indexes are computed before the encryption pass, since the raw
  values are unavailable afterwards.
- passenger profile update and admin driver update refresh the index when
  the underlying field changes. For the composite name index the untouched
  half is read back from the row.

Adds --audit to the backfill script: recomputes every index from its
encrypted value and reports missing or stale entries. Drift here is silent
by nature — it surfaces only when a real search fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 16:04:40 +03:00
Hamza-AyedandClaude Opus 5 15f55ff3e4 Add --verify to the backfill script
Confirms a value is findable through the blind index after backfilling,
without opening the console or the database. Prints matched row ids only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 15:59:50 +03:00
Hamza-AyedandClaude Opus 5 761b957c96 Silence REQUEST_METHOD warning when bootstrap runs from the CLI
The migration and backfill scripts load bootstrap.php outside a request, where
$_SERVER['REQUEST_METHOD'] does not exist, so every CLI run printed a warning
before its output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 15:55:18 +03:00
Hamza-AyedandClaude Opus 5 57e22477fb Add an idempotent CLI migration runner
phpMyAdmin is unreachable on this deployment, so schema changes need a path
that does not depend on it. migrate.php reuses core/Database, meaning no
credentials are passed on the command line, and checks information_schema
before each ALTER so re-running is safe and never fails on a duplicate
column. Supports --status and --dry-run.

Covers the blind-index columns and the missing adminUser status/approved_by/
approved_at columns. Every change is additive and nullable, so applying it to
a running database changes no behaviour on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 15:35:34 +03:00
Hamza-AyedandClaude Opus 5 6802026dbd Add blind-index search layer; fix captain detail 200-with-empty-body
Searching encrypted columns currently works only because encryptData() is
AES-CBC with a fixed IV, i.e. deterministic. That determinism is what leaks
equality and shared prefixes, and it is why moving storage to AES-GCM would
break every lookup. This separates the two concerns.

- core/Security/BlindIndex.php: HMAC-SHA256 over a normalised value, keyed by
  a secret pepper. Phone numbers have a small keyspace, so a bare SHA-256
  would be reversible by enumeration; the pepper lives in the environment, not
  the database. The scope string includes table and field so the same number
  does not produce a matching index across tables.
  Normalisation unifies local/international phone forms, lowercases emails and
  folds Arabic alef/ya/ta-marbuta and diacritics for names.
- migrations/: nullable *_bidx columns plus indexes, and the missing
  adminUser.status/approved_by/approved_at columns that admin approvals need.
- scripts/backfill_blind_index.php: restartable, batched, --dry-run capable,
  touches only index columns.
- Admin lookups by phone/email now match the index, keeping the old ciphertext
  comparison in the same query so search keeps working until the backfill runs.
  bootstrap exposes $blindIndex as null when no pepper is configured.

Also: AdminCaptain/getCaptainDetailsById.php selected driver.education, a
column absent from this schema. The PDOException was uncaught, so the client
received an empty body with HTTP 200 — the "non-JSON response" seen when
opening a captain. It now omits the column, catches the error, reports it as
JSON, and requires an admin role.

Console: opening any sidebar section refetches its data instead of showing
what was loaded when the console started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 15:16:09 +03:00
Hamza-Ayed 42f6d33efd Update: 2026-07-25 14:44:30 2026-07-25 14:44:30 +03:00
Hamza-AyedandClaude Opus 5 4009af8dd3 Add campaign launcher and app version manager; fix SQL injection in updatePackages
serviceapp/updatePackages.php built its UPDATE by interpolating the request
values straight into the SQL string, so any caller with a valid token could
execute arbitrary SQL through the version field. It now uses bound
parameters, requires an admin role, validates the version format, and writes
an audit entry.

trigger_campaign.php gains dry_run=1: it performs the same Gemini analysis
and target selection but returns before creating the promo code and before
dispatching any notification. Launching without previewing was the only
option before, and a launch writes a seven-day discount and pushes to every
passenger in the country.

Console:
- Campaign launcher with a mandatory preview. Launching stays disabled until
  the current parameters have been previewed, and re-locks if any parameter
  changes afterwards or once a launch completes.
- App version manager with the same version-format check as the server and a
  confirmation naming the old and new values.

Cache busting: assets are served straight off a bind mount with no version,
so browsers kept running the previously cached build after a deploy. Both
asset links now carry ?v=, and the build id is shown in Session & Security
and printed in the diagnostics report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 14:41:27 +03:00
Hamza-AyedandClaude Opus 5 a0ab6c5155 Add broadcast notifications and transit route approvals
Broadcast: ride/firebase/send_fcm.php is an internal service guarded by a
shared secret, so the browser cannot call it — holding that key client-side
would expose it, and the endpoint cannot tell who the sender is. A new
Admin/notifications/broadcast.php sits in front of it: it runs behind
connect.php, requires super_admin, restricts the target to the two topics the
apps actually subscribe to ('drivers'/'passengers') so it cannot be used to
push to an arbitrary topic or a single device token, bounds the title and
body, writes an audit entry before dispatching, and only then forwards the
call internally with the shared secret.

The composer shows a live push preview and an explicit confirmation naming
the audience, since a broadcast cannot be recalled.

Route approvals: draft routes render with their stops, distance and stop
count, and approve/reject posts to transit/route/approve.php behind a
confirmation stating the consequence. Available to admins and super admins,
matching the endpoint's own role check.

Also render user-supplied text with unicode-bidi: plaintext — Arabic names,
addresses and messages were being laid out left-to-right inside the
English UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 02:15:02 +03:00
Hamza-AyedandClaude Opus 5 db4ca7dd7a Fix dashbord.php parse error; require super_admin on pricing and crypto tools
Hotfix: a comment added to the dashboard SQL contained double quotes inside
the double-quoted PHP string, terminating it and making dashbord.php fail to
parse. Production was returning a parse error for every dashboard request.

Authorisation gaps closed — connect.php only proves a token is valid, it does
not check what the caller is allowed to do:

- Admin/ggg.php decrypts any database field and was authorised solely by an
  admin phone number sent in the request body. Anyone who knew a listed
  number could decrypt platform data without signing in. It now runs behind
  connect.php, requires super_admin, keeps the phone list as a second factor,
  and records every use.
- ride/kazan/update.php, kazan/add.php and ride/promo/{add,update,delete}.php
  changed live pricing and discount codes with no role check at all, so any
  valid token — including a driver's or passenger's — could rewrite the fare
  table. All now require super_admin.

Staff/pending.php: adminUser has no `status` column in this deployment, so
the query failed with an opaque "unavailable". It now checks for the column
and reports the actual reason.

Console: Kazan tariff editor for super admins — sends only changed fields,
shows an old → new confirmation before saving, and stays read-only with an
explanatory notice for plain admins.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 02:00:14 +03:00
Hamza-AyedandClaude Opus 5 852c6ece5c Fix admin status handling for the current ride pipeline; extend console
The ride table holds two generations of status values: the legacy CamelCase
set ('Finished', 'CancelFromPassenger') and the lowercase set written by
backend/ride/rides/* today ('completed', 'cancelled_by_passenger'). Admin
queries only matched the legacy set, so on live data:

- get_rides_by_status.php returned nothing meaningful for every filter, and
  the "in progress" default masked it.
- dashbord.php reported total_driver_earnings as NULL, completed_rides as a
  fraction of the real count, and cancelled_rides as 0.
- driver_avg_duration averaged in negative durations, yielding "-00h 22m".

All three now match on LOWER(status) across both families.

Staff/pending.php ran with no authentication at all, exposing pending
admins' names and phone numbers to any caller; it now goes through
connect.php with a role check. It also returned HTTP 400 for everything when
the `users` table was absent — each source is queried independently and
reports its own availability.

Console:
- Render rides from either schema generation (price/date/time and
  start_location coordinates, or the older address/created_at columns).
- Null aggregates render as "—" rather than a measured 0.00.
- Add tariff/promo, WhatsApp send and encryption modules, all super-admin
  gated; pricing remains read-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 01:47:55 +03:00
Hamza-Ayed 10f1154cc7 Update adminUser fingerprint and fingerprint_hash in database upon successful OTP verification 2026-07-25 00:30:14 +03:00