enforce(RateLimiter::identifier(), 'login', maxAttempts: 5, windowSeconds: 60); // تتبع المحاولات الفاشلة لكل حساب لمنع credential stuffing عبر IPs متعددة if ($redis && !empty($phone)) { $accountKey = "login_attempts:account:" . hash('sha256', $phone); $accountAttempts = (int) $redis->get($accountKey); if ($accountAttempts >= 5) { $ttl = $redis->ttl($accountKey); $waitMinutes = ceil($ttl / 60); jsonError("تم تعليق تسجيل الدخول لهذا الحساب مؤقتاً. يرجى المحاولة بعد {$waitMinutes} دقيقة."); exit; } } // تسجيل محاولة تسجيل الدخول للتدقيق $loginAuditData = [ 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'fingerprint_hash' => $fpHash ?? null, 'phone_hash' => !empty($phone) ? hash('sha256', $phone) : null, 'timestamp' => date('Y-m-d H:i:s'), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 'result' => 'pending' ]; error_log("[LOGIN_AUDIT] " . json_encode($loginAuditData)); try { $con = Database::get('main'); // البحث عن المشرف باستخدام بصمة الجهاز (Fingerprint Hash) $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); // إذا لم يتم العثور بالبصمة، وتم تمرير رقم الهاتف (تسجيل دخول لأول مرة أو جهاز جديد) if (!$admin && !empty($phone)) { $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); // تأكيد كلمة المرور وتحديث بصمة الجهاز إذا تم إيجاد الحساب if ($admin && password_verify($password, $admin['password'])) { $encFpRaw = $encryptionHelper->encryptData($fingerprint); $updateStmt = $con->prepare("UPDATE adminUser SET fingerprint = :fp_raw, fingerprint_hash = :fp WHERE id = :id"); $updateStmt->execute([ ':fp_raw' => $encFpRaw, ':fp' => $fpHash, ':id' => $admin['id'] ]); $admin['fingerprint_hash'] = $fpHash; // Update locally } else if ($admin) { // Password incorrect, fail later. } } if ($admin) { // 1. التحقق من حالة الحساب if ($admin['status'] === 'pending') { jsonError("حسابك قيد المراجعة حالياً. يرجى الانتظار للموافقة."); exit; } elseif ($admin['status'] === 'suspended') { jsonError("هذا الحساب معلق. يرجى التواصل مع المدير."); exit; } elseif ($admin['status'] === 'rejected') { jsonError("تم رفض طلب الانضمام لهذا الحساب."); exit; } // 2. التحقق من كلمة المرور if (password_verify($password, $admin['password'])) { // إذا كان هذا مجرد تجديد للتوكن (إعادة الدخول التلقائي من التطبيق)، فلا داعي لإرسال OTP if ($isRenewal) { $jwtService = new JwtService($redis); $role = $admin['role'] ?? 'admin'; // إلغاء التوكن القديم إذا وجد في Redis if ($redis) { $oldJti = $redis->get("active_jti:" . $admin['id']); if ($oldJti) { $jwtService->revokeToken($oldJti, 3600); } } $jwt = $jwtService->generateAccessToken($admin['id'], $role, $audience, $fingerprint); // فك تشفير البيانات للعرض $admin['name'] = $encryptionHelper->decryptData($admin['name']) ?: $admin['name']; unset($admin['password']); printSuccess([ "message" => "Login successful", "admin" => $admin, "jwt" => $jwt, "expires_in" => 3600 ]); exit; } // 3. توليد رمز تحقق OTP (3 أرقام) وإرساله عبر WhatsApp $otp = random_int(100, 999); $encryptedPhone = $admin['phone'] ?? ''; if (empty($encryptedPhone)) { jsonError("رقم الهاتف غير مسجل لهذا الحساب. يرجى مراجعة الإدارة."); exit; } // فك تشفير رقم الهاتف (مخزن مشفراً في قاعدة البيانات) $phone = $encryptionHelper->decryptData($encryptedPhone); if (!$phone || empty($phone)) { // إذا فشل فك التشفير، قد يكون الرقم مخزناً بدون تشفير $phone = $encryptedPhone; } $messageBody = "رمز التحقق الخاص بك للدخول إلى لوحة الإدارة هو: $otp"; $success = sendWhatsAppFromServer($phone, $messageBody); if ($success) { // تخزين هاش للـ OTP بدلاً من النص الصريح $otpHash = hash('sha256', (string)$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 = substr($phone, 0, 4) . '****' . substr($phone, -3); printSuccess([ "status" => "otp_required", "message" => "تم إرسال رمز التحقق إلى WhatsApp الخاص بك.", "phone" => $maskedPhone ]); } else { jsonError("فشل في إرسال رمز التحقق عبر WhatsApp."); } } else { jsonError("كلمة المرور غير صحيحة."); } } else { jsonError("الحساب أو الجهاز غير مسجل. يرجى إدخال رقم هاتفك وكلمة المرور إذا كان هذا أول تسجيل دخول لك."); } } catch (Exception $e) { error_log("[Admin Login Error] " . $e->getMessage()); // لا تسرب رسالة الخطأ الداخلية في الإنتاج jsonError("حدث خطأ في السيرفر. يرجى المحاولة لاحقاً."); }