redis = $redis; } // ── توليد وحفظ OTP ───────────────────────────────────── // $digits: عدد الأرقام — الافتراضي 6، يمكن تمرير 3 لـ transit (100–999) public function generate(string $phone, int $digits = 3): string { $min = (int)str_pad('1', $digits, '0'); // digits=3 → 100 | digits=6 → 100000 $max = (int)str_pad('9', $digits, '9'); // digits=3 → 999 | digits=6 → 999999 $otp = str_pad((string)random_int($min, $max), $digits, '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}"); } }