diff --git a/backend/Admin/auth/login.php b/backend/Admin/auth/login.php index 3fca6fa1..4d442165 100644 --- a/backend/Admin/auth/login.php +++ b/backend/Admin/auth/login.php @@ -1,7 +1,7 @@ enforce(RateLimiter::identifier(), 'login'); -// تتبع المحاولات الفاشلة لكل حساب لمنع credential stuffing عبر IPs متعددة +// تتبع المحاولات الفاشلة لكل حساب if ($redis && !empty($phone)) { $accountKey = "login_attempts:account:" . hash('sha256', $phone); $accountAttempts = (int) $redis->get($accountKey); @@ -64,24 +64,29 @@ try { $stmt->execute([':fp' => $fpHash]); $admin = $stmt->fetch(PDO::FETCH_ASSOC); - // إذا لم يتم العثور بالبصمة، وتم تمرير اسم المستخدم/الإيميل/الهاتف (تسجيل دخول لأول مرة أو جهاز جديد) + // إذا لم يتم العثور بالبصمة، نبحث بالـ ID المباشر أو بفك تشفير البيانات (AES-GCM Decryption in PHP) if (!$admin && !empty($phone)) { - // 1. تجربة البحث بالهاتف المشفّر - if ($encryptionHelper) { - $encPhoneInput = $encryptionHelper->encryptData($phone); - $stmtPhone = $con->prepare("SELECT * FROM adminUser WHERE phone = :phone LIMIT 1"); - $stmtPhone->execute([':phone' => $encPhoneInput]); - $admin = $stmtPhone->fetch(PDO::FETCH_ASSOC); - } - - // 2. إذا لم يجد بالهاتف المشفّر، نجرب بالبريد الإلكتروني أو المعرّف (id / email) أو رقم الهاتف غير المشفّر + // 1. بحث مباشر بالـ ID المعياري Unencrypted + $stmtId = $con->prepare("SELECT * FROM adminUser WHERE id = :id LIMIT 1"); + $stmtId->execute([':id' => $phone]); + $admin = $stmtId->fetch(PDO::FETCH_ASSOC); + + // 2. إذا لم يتم العثور بالـ ID، نفحص الحقول المشفّرة (email / phone / name) عبر فك التشفير if (!$admin) { - $stmtAlt = $con->prepare("SELECT * FROM adminUser WHERE email = :input OR id = :input OR phone = :input LIMIT 1"); - $stmtAlt->execute([':input' => $phone]); - $admin = $stmtAlt->fetch(PDO::FETCH_ASSOC); + $stmtAll = $con->query("SELECT * FROM adminUser"); + while ($row = $stmtAll->fetch(PDO::FETCH_ASSOC)) { + $decPhone = ($encryptionHelper && !empty($row['phone'])) ? $encryptionHelper->decryptData($row['phone']) : $row['phone']; + $decEmail = ($encryptionHelper && !empty($row['email'])) ? $encryptionHelper->decryptData($row['email']) : $row['email']; + $decName = ($encryptionHelper && !empty($row['name'])) ? $encryptionHelper->decryptData($row['name']) : $row['name']; + + if ($phone === $decPhone || $phone === $decEmail || $phone === $decName || $phone === $row['phone'] || $phone === $row['email']) { + $admin = $row; + break; + } + } } - // تأكيد كلمة المرور وتحديث بصمة الجهاز إذا تم إيجاد الحساب + // عند إيجاد الحساب وتأكيد كلمة المرور، نقوم بتحديث وبث بصمة الجهاز للجلسة if ($admin && password_verify($password, $admin['password'])) { $encFpRaw = $encryptionHelper ? $encryptionHelper->encryptData($fingerprint) : $fingerprint; $updateStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id"); @@ -90,9 +95,7 @@ try { ':fp' => $fpHash, ':id' => $admin['id'] ]); - $admin['fingerprint_hash'] = $fpHash; // Update locally - } else if ($admin) { - // Password incorrect, fail later. + $admin['fingerprint_hash'] = $fpHash; } } @@ -114,41 +117,69 @@ try { // 2. التحقق من كلمة المرور if (password_verify($password, $admin['password'])) { - // إصدار التوكن النهائي للمشرف - $jwtService = new JwtService($redis); - $role = $admin['role'] ?? 'admin'; - - // إلغاء التوكن القديم إذا وجد في Redis - if ($redis) { - $oldJti = $redis->get("active_jti:" . $admin['id']); - if ($oldJti) { - $jwtService->revokeToken($oldJti, 3600); + // إذا كان تجديد توكن تلقائي من التطبيق/الجهاز الموثوق + if ($isRenewal) { + $jwtService = new JwtService($redis); + $role = $admin['role'] ?? 'admin'; + + 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 + ]); + exit; } - $jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint); + // 3. توليد رمز تحقق OTP (3 أرقام) وإرساله عبر نظام OTP الموحد (Nabeh API) + $otp = (string)random_int(100, 999); + $encryptedPhone = $admin['phone'] ?? ''; - // فك تشفير البيانات للعرض - if ($encryptionHelper && !empty($admin['name'])) { - $admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name']; + $rawPhone = ($encryptionHelper && !empty($encryptedPhone)) ? $encryptionHelper->decryptData($encryptedPhone) : $encryptedPhone; + if (!$rawPhone || empty($rawPhone)) { + $rawPhone = $encryptedPhone; } - unset($admin['password']); + + // استدعاء موزع خدمات OTP عبر Nabeh API + require_once __DIR__ . '/../../auth/otp/providers.php'; + $success = sendNabehOtp($rawPhone, $otp, 'whatsapp', 'admin'); + + // تخزين OTP (SHA-256 hash) في جدول token_verification_admin + $otpHash = hash('sha256', $otp); + $stmt = $con->prepare("INSERT INTO token_verification_admin (phone_number, token, expiration_time) + VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE)) + ON DUPLICATE KEY UPDATE token = VALUES(token), expiration_time = VALUES(expiration_time)"); + $stmt->execute([$encryptedPhone, $otpHash]); + + $maskedPhone = (strlen($rawPhone) > 7) ? substr($rawPhone, 0, 4) . '****' . substr($rawPhone, -3) : $rawPhone; printSuccess([ - "message" => "Login successful", - "admin" => $admin, - "jwt" => $jwt, - "expires_in" => 3600 + "status" => "otp_required", + "message" => $success ? "تم إرسال رمز التحقق إلى WhatsApp الخاص بك." : "فشل إرسال واتساب. تحقق من error_log لمعرفة OTP.", + "phone" => $maskedPhone ]); exit; } else { jsonError("كلمة المرور غير صحيحة."); } } else { - jsonError("الحساب غير موجود. يرجى التأكد من البريد الإلكتروني أو رقم الهاتف وكلمة المرور."); + jsonError("الحساب غير موجود. يرجى التأكد من اسم المستخدم أو البريد الإلكتروني وكلمة المرور."); } } catch (Throwable $e) { error_log("[Admin Login Throwable Error] " . $e->getMessage() . "\nTrace: " . $e->getTraceAsString()); jsonError("حدث خطأ في السيرفر: " . $e->getMessage(), 500); } -