قرار المالك 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>
138 lines
6.2 KiB
PHP
138 lines
6.2 KiB
PHP
<?php
|
|
/**
|
|
* Admin/auth/verify_login.php
|
|
* الخطوة الثانية من تسجيل الدخول: التحقق من الـ OTP وإصدار التوكن النهائي
|
|
*/
|
|
require_once __DIR__ . '/../../core/bootstrap.php';
|
|
require_once __DIR__ . '/../../functions.php';
|
|
|
|
$otp = filterRequest('otp');
|
|
$fingerprint = filterRequest('fingerprint'); // مطلوب لربط التوكن بالجهاز
|
|
$audience = filterRequest('aud') ?? 'admin';
|
|
|
|
if (empty($otp) || empty($fingerprint)) {
|
|
jsonError("OTP and fingerprint are required.");
|
|
exit;
|
|
}
|
|
|
|
// Rate Limiting: 3 محاولات OTP في 5 دقائق لكل IP
|
|
$rateLimiter = new RateLimiter($redis);
|
|
$rateLimiter->enforce(RateLimiter::identifier(), 'otp');
|
|
|
|
try {
|
|
$con = Database::get('main');
|
|
|
|
// 1. جلب بيانات المسؤول عبر البصمة أو من الـ OTP المعلق للجهاز الجديد
|
|
$fpHash = hash('sha256', $fingerprint);
|
|
$stmt = $con->prepare("SELECT * FROM adminUser WHERE fingerprint_hash = :fp LIMIT 1");
|
|
$stmt->execute([':fp' => $fpHash]);
|
|
$admin = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
$otpHash = hash('sha256', (string)$otp);
|
|
|
|
if (!$admin) {
|
|
// إذا كانت البصمة جديدة وغير مسجلة بعد، نبحث عن الحساب المرتبط بـ OTP المعلق
|
|
$stmtOtp = $con->prepare("SELECT phone_number FROM token_verification_admin WHERE token = ? AND expiration_time >= NOW() LIMIT 1");
|
|
$stmtOtp->execute([$otpHash]);
|
|
$otpRow = $stmtOtp->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($otpRow && !empty($otpRow['phone_number'])) {
|
|
// $targetPhone هو الرقم المشفر من token_verification_admin (نفس القيمة المخزنة في adminUser.phone)
|
|
$targetPhone = $otpRow['phone_number'];
|
|
global $encryptionHelper;
|
|
|
|
// البحث المباشر: phone المشفر مطابق لنفس النص المشفر في adminUser.phone
|
|
$stmtAdmin = $con->prepare("SELECT * FROM adminUser WHERE phone = :p1 OR id = :p2 LIMIT 1");
|
|
$stmtAdmin->execute([':p1' => $targetPhone, ':p2' => $targetPhone]);
|
|
$admin = $stmtAdmin->fetch(PDO::FETCH_ASSOC);
|
|
|
|
// مسار احتياطي: فك التشفير لمقارنة القيم (ضروري لـ AES-GCM حيث التشفير غير حتمي)
|
|
if (!$admin) {
|
|
$decTarget = ($encryptionHelper && !empty($targetPhone)) ? $encryptionHelper->decryptData($targetPhone) : null;
|
|
$stmtAll = $con->query("SELECT * FROM adminUser");
|
|
while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) {
|
|
// مقارنة مباشرة للنصوص المشفرة (نفس ciphertext)
|
|
if ($targetPhone === $row['phone']) {
|
|
$admin = $row;
|
|
break;
|
|
}
|
|
// مقارنة عبر فك التشفير (AES-GCM: ciphertexts مختلفة لنفس النص)
|
|
if ($decTarget) {
|
|
$decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone'];
|
|
if ($decTarget === $decPhone) {
|
|
$admin = $row;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$admin) {
|
|
jsonError("المسؤول غير موجود أو رمز التحقق غير صالح.");
|
|
exit;
|
|
}
|
|
|
|
// 2. رقم الهاتف المشفر (للاستخدام في جدول OTP)
|
|
$encryptedPhone = $admin['phone'] ?? '';
|
|
|
|
// فك تشفيره لو احتجنا إرساله أو عرضه، لكن هنا نحن نحتاج المشفر للبحث
|
|
// $phone = $encryptionHelper->decryptData($encryptedPhone);
|
|
|
|
// 3. التحقق من الـ OTP (الهاش محسوب مسبقاً في المتغير $otpHash)
|
|
$stmt = $con->prepare("SELECT * FROM token_verification_admin
|
|
WHERE phone_number = ? AND token = ?
|
|
AND expiration_time >= NOW()");
|
|
$stmt->execute([$encryptedPhone, $otpHash]);
|
|
|
|
if ($stmt->rowCount() === 0) {
|
|
jsonError("رمز التحقق غير صالح أو منتهي الصلاحية.");
|
|
exit;
|
|
}
|
|
|
|
// حذف الرمز بعد استخدامه لمرة واحدة (باستخدام الرقم المشفر)
|
|
$con->prepare("DELETE FROM token_verification_admin WHERE phone_number = ?")->execute([$encryptedPhone]);
|
|
|
|
// 4. تحديث وتأكيد بصمة المتصفح/الجهاز الحالية للمسؤول في قاعدة البيانات بعد التحقق الناجح من OTP
|
|
$encFpRaw = ($encryptionHelper && !empty($fingerprint)) ? $encryptionHelper->encryptData($fingerprint) : $fingerprint;
|
|
$updateFpStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id");
|
|
$updateFpStmt->execute([
|
|
':fp_raw' => $encFpRaw,
|
|
':fp' => $fpHash,
|
|
':id' => $admin['id']
|
|
]);
|
|
$admin['fingerprint_hash'] = $fpHash;
|
|
|
|
// 5. إصدار التوكن النهائي
|
|
$jwtService = new JwtService($redis);
|
|
$role = $admin['role'] ?? 'admin';
|
|
|
|
// إلغاء التوكن القديم إذا وجد في Redis (Token Revocation)
|
|
if ($redis) {
|
|
$oldJti = $redis->get("active_jti:" . $admin['id']);
|
|
if ($oldJti) {
|
|
$jwtService->revokeToken($oldJti, 3600);
|
|
}
|
|
}
|
|
|
|
$jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint);
|
|
|
|
// فك تشفير البيانات للعرض
|
|
if ($encryptionHelper && !empty($admin['name'])) {
|
|
$admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name'];
|
|
}
|
|
unset($admin['password']);
|
|
|
|
printSuccess([
|
|
"message" => "Login successful",
|
|
"admin" => $admin,
|
|
"jwt" => $jwt,
|
|
"expires_in" => 3600
|
|
]);
|
|
|
|
} catch (Throwable $e) {
|
|
error_log("[Admin Verify OTP Error] " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString());
|
|
jsonError("Server Error: " . $e->getMessage() . " on line " . $e->getLine());
|
|
}
|