200 lines
6.4 KiB
Python
200 lines
6.4 KiB
Python
import redis.asyncio as redis
|
|
from typing import Optional
|
|
import logging
|
|
import json
|
|
|
|
from config.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RedisService:
|
|
_instance: Optional["RedisService"] = None
|
|
_client: Optional[redis.Redis] = None
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
return cls._instance
|
|
|
|
async def connect(self) -> None:
|
|
if self._client is not None:
|
|
return
|
|
|
|
try:
|
|
self._client = redis.from_url(
|
|
settings.redis_url,
|
|
password=settings.redis_password,
|
|
encoding="utf-8",
|
|
decode_responses=True,
|
|
socket_timeout=5.0,
|
|
socket_connect_timeout=5.0,
|
|
)
|
|
|
|
await self._client.ping()
|
|
logger.info("Connexion Redis etablie")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur connexion Redis: {e}")
|
|
self._client = None
|
|
raise
|
|
|
|
async def disconnect(self) -> None:
|
|
if self._client:
|
|
await self._client.close()
|
|
self._client = None
|
|
logger.info("Connexion Redis fermee")
|
|
|
|
async def is_connected(self) -> bool:
|
|
if not self._client:
|
|
return False
|
|
try:
|
|
await self._client.ping()
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
@property
|
|
def client(self) -> redis.Redis:
|
|
if not self._client:
|
|
raise RuntimeError("Redis non connecte. Appelez connect() d'abord.")
|
|
return self._client
|
|
|
|
async def blacklist_token(self, token_id: str, ttl_seconds: int) -> bool:
|
|
try:
|
|
key = f"{settings.token_blacklist_prefix}{token_id}"
|
|
await self.client.setex(key, ttl_seconds, "1")
|
|
logger.debug(f"Token {token_id[:8]}... ajoute a la blacklist")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Erreur blacklist token: {e}")
|
|
return False
|
|
|
|
async def is_token_blacklisted(self, token_id: str) -> bool:
|
|
try:
|
|
key = f"{settings.token_blacklist_prefix}{token_id}"
|
|
result = await self.client.exists(key)
|
|
return result > 0
|
|
except Exception as e:
|
|
logger.error(f"Erreur verification blacklist: {e}")
|
|
return False
|
|
|
|
async def blacklist_user_tokens(
|
|
self, user_id: str, ttl_seconds: int = 86400
|
|
) -> bool:
|
|
try:
|
|
key = f"{settings.token_blacklist_prefix}user:{user_id}"
|
|
import time
|
|
|
|
await self.client.setex(key, ttl_seconds, str(int(time.time())))
|
|
logger.info(f"Tokens utilisateur {user_id} invalides")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Erreur invalidation tokens utilisateur: {e}")
|
|
return False
|
|
|
|
async def get_user_token_invalidation_time(self, user_id: str) -> Optional[int]:
|
|
try:
|
|
key = f"{settings.token_blacklist_prefix}user:{user_id}"
|
|
result = await self.client.get(key)
|
|
return int(result) if result else None
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture invalidation: {e}")
|
|
return None
|
|
|
|
async def increment_rate_limit(self, key: str, window_seconds: int) -> int:
|
|
try:
|
|
full_key = f"{settings.rate_limit_prefix}{key}"
|
|
|
|
pipe = self.client.pipeline()
|
|
pipe.incr(full_key)
|
|
pipe.expire(full_key, window_seconds)
|
|
results = await pipe.execute()
|
|
|
|
return results[0]
|
|
except Exception as e:
|
|
logger.error(f"Erreur increment rate limit: {e}")
|
|
return 0
|
|
|
|
async def get_rate_limit_count(self, key: str) -> int:
|
|
try:
|
|
full_key = f"{settings.rate_limit_prefix}{key}"
|
|
result = await self.client.get(full_key)
|
|
return int(result) if result else 0
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture rate limit: {e}")
|
|
return 0
|
|
|
|
async def reset_rate_limit(self, key: str) -> bool:
|
|
try:
|
|
full_key = f"{settings.rate_limit_prefix}{key}"
|
|
await self.client.delete(full_key)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Erreur reset rate limit: {e}")
|
|
return False
|
|
|
|
async def store_refresh_token_metadata(
|
|
self, token_id: str, user_id: str, fingerprint_hash: str, ttl_seconds: int
|
|
) -> bool:
|
|
try:
|
|
key = f"refresh_token:{token_id}"
|
|
data = json.dumps(
|
|
{
|
|
"user_id": user_id,
|
|
"fingerprint_hash": fingerprint_hash,
|
|
"used": False,
|
|
}
|
|
)
|
|
await self.client.setex(key, ttl_seconds, data)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Erreur stockage metadata refresh token: {e}")
|
|
return False
|
|
|
|
async def get_refresh_token_metadata(self, token_id: str) -> Optional[dict]:
|
|
try:
|
|
key = f"refresh_token:{token_id}"
|
|
data = await self.client.get(key)
|
|
return json.loads(data) if data else None
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture metadata refresh token: {e}")
|
|
return None
|
|
|
|
async def mark_refresh_token_used(self, token_id: str) -> bool:
|
|
try:
|
|
key = f"refresh_token:{token_id}"
|
|
data = await self.client.get(key)
|
|
if not data:
|
|
return False
|
|
|
|
metadata = json.loads(data)
|
|
metadata["used"] = True
|
|
metadata["used_at"] = int(__import__("time").time())
|
|
|
|
ttl = await self.client.ttl(key)
|
|
if ttl > 0:
|
|
await self.client.setex(key, ttl, json.dumps(metadata))
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Erreur marquage refresh token: {e}")
|
|
return False
|
|
|
|
async def delete_refresh_token(self, token_id: str) -> bool:
|
|
try:
|
|
key = f"refresh_token:{token_id}"
|
|
result = await self.client.delete(key)
|
|
return result > 0
|
|
except Exception as e:
|
|
logger.error(f"Erreur suppression refresh token: {e}")
|
|
return False
|
|
|
|
|
|
redis_service = RedisService()
|
|
|
|
|
|
async def get_redis() -> RedisService:
|
|
if not await redis_service.is_connected():
|
|
await redis_service.connect()
|
|
return redis_service
|