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()); }