redis = $redis; } // ── توليد وحفظ OTP ───────────────────────────────────── public function generate(string $phone): string { // OTP آمن (6 أرقام عشوائية) $otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT); if ($this->redis) { $key = "otp:{$phone}"; $this->redis->setex($key, self::OTP_TTL, password_hash($otp, PASSWORD_BCRYPT)); // إعادة تعيين عداد المحاولات $this->redis->del("otp:attempts:{$phone}"); } return $otp; } // ── التحقق من OTP ─────────────────────────────────────── public function verify(string $phone, string $inputOtp): bool { if (!$this->redis) return false; // فحص الـ lockout if ($this->redis->exists("otp:locked:{$phone}")) { return false; } $key = "otp:{$phone}"; $stored = $this->redis->get($key); if (!$stored) { return false; // انتهت صلاحية الـ OTP } $attemptsKey = "otp:attempts:{$phone}"; if (!password_verify($inputOtp, $stored)) { $attempts = $this->redis->incr($attemptsKey); $this->redis->expire($attemptsKey, self::OTP_TTL); if ($attempts >= self::MAX_ATTEMPTS) { // قفل لمدة 30 دقيقة $this->redis->setex("otp:locked:{$phone}", self::LOCKOUT_TTL, '1'); $this->redis->del($key); } return false; } // نجح التحقق — احذف الـ OTP $this->redis->del($key); $this->redis->del($attemptsKey); return true; } // ── فحص هل الرقم مقفل ────────────────────────────────── public function isLocked(string $phone): bool { return $this->redis && (bool)$this->redis->exists("otp:locked:{$phone}"); } }