# πŸ” АутСнтификация для микросСрвисов ## 🎯 ΠžΠ±Π·ΠΎΡ€ Руководство ΠΏΠΎ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ систСмы Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ Discours Core с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ микросСрвисами Ρ‡Π΅Ρ€Π΅Π· ΠΎΠ±Ρ‰ΠΈΠΉ Redis connection pool. ## πŸš€ Быстрый старт ### ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΊ Redis ```python # Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ Ρ‚ΠΎΡ‚ ΠΆΠ΅ Redis connection pool from storage.redis import redis # Или создайтС свой с Ρ‚Π΅ΠΌΠΈ ΠΆΠ΅ настройками import aioredis redis_client = aioredis.from_url( "redis://localhost:6379/0", max_connections=20, retry_on_timeout=True, socket_keepalive=True, socket_keepalive_options={}, health_check_interval=30 ) ``` ### ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΎΠΊΠ΅Π½Π° сСссии ```python from auth.tokens.sessions import SessionTokenManager from auth.utils import extract_token_from_request async def check_user_session(request) -> dict | None: """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° сСссии ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² микросСрвисС""" # 1. ИзвлСкаСм Ρ‚ΠΎΠΊΠ΅Π½ ΠΈΠ· запроса token = await extract_token_from_request(request) if not token: return None # 2. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ сСссию Ρ‡Π΅Ρ€Π΅Π· SessionTokenManager sessions = SessionTokenManager() payload = await sessions.verify_session(token) if payload: return { "authenticated": True, "user_id": payload.get("user_id"), "username": payload.get("username"), "expires_at": payload.get("exp") } return {"authenticated": False, "error": "Invalid token"} ``` ## πŸ”‘ Redis ΠΊΠ»ΡŽΡ‡ΠΈ для поиска ### Π‘Ρ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° Π΄Π°Π½Π½Ρ‹Ρ… ```bash # БСссии ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ session:{user_id}:{token} # Hash: {user_id, username, device_info, last_activity} user_sessions:{user_id} # Set: {token1, token2, ...} # OAuth Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ oauth_access:{user_id}:{provider} # JSON: {token, expires_in, scope} oauth_refresh:{user_id}:{provider} # JSON: {token, provider_data} # Π’ΠΎΠΊΠ΅Π½Ρ‹ подтвСрТдСния verification_token:{token} # JSON: {user_id, type, data, created_at} # OAuth состояниС oauth_state:{state} # JSON: {provider, redirect_uri, code_verifier} ``` ### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ поиска ```python from storage.redis import redis # 1. Поиск всСх сСссий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ async def get_user_sessions(user_id: int) -> list[str]: """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" session_key = f"user_sessions:{user_id}" tokens = await redis.smembers(session_key) return [token.decode() for token in tokens] if tokens else [] # 2. ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΠΎΠΉ сСссии async def get_session_data(user_id: int, token: str) -> dict | None: """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ сСссии""" session_key = f"session:{user_id}:{token}" data = await redis.hgetall(session_key) if data: return {k.decode(): v.decode() for k, v in data.items()} return None # 3. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° сущСствования Ρ‚ΠΎΠΊΠ΅Π½Π° async def token_exists(user_id: int, token: str) -> bool: """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ сущСствованиС Ρ‚ΠΎΠΊΠ΅Π½Π°""" session_key = f"session:{user_id}:{token}" return await redis.exists(session_key) # 4. ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ TTL Ρ‚ΠΎΠΊΠ΅Π½Π° async def get_token_ttl(user_id: int, token: str) -> int: """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ врСмя ΠΆΠΈΠ·Π½ΠΈ Ρ‚ΠΎΠΊΠ΅Π½Π° Π² сСкундах""" session_key = f"session:{user_id}:{token}" return await redis.ttl(session_key) ``` ## πŸ› οΈ ΠœΠ΅Ρ‚ΠΎΠ΄Ρ‹ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ ### 1. ΠŸΡ€ΡΠΌΠ°Ρ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΎΠΊΠ΅Π½Π° ```python from auth.tokens.sessions import SessionTokenManager async def authenticate_request(request) -> dict: """АутСнтификация запроса Π² микросСрвисС""" sessions = SessionTokenManager() # ИзвлСкаСм Ρ‚ΠΎΠΊΠ΅Π½ token = await extract_token_from_request(request) if not token: return {"authenticated": False, "error": "No token provided"} try: # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ JWT ΠΈ Redis сСссию payload = await sessions.verify_session(token) if payload: user_id = payload.get("user_id") # Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π΄Π°Π½Π½Ρ‹Π΅ сСссии ΠΈΠ· Redis session_data = await sessions.get_session_data(token, user_id) return { "authenticated": True, "user_id": user_id, "username": payload.get("username"), "session_data": session_data, "expires_at": payload.get("exp") } else: return {"authenticated": False, "error": "Invalid or expired token"} except Exception as e: return {"authenticated": False, "error": f"Authentication error: {str(e)}"} ``` ### 2. Массовая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ```python from auth.tokens.batch import BatchTokenOperations async def validate_multiple_tokens(tokens: list[str]) -> dict[str, bool]: """Массовая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² для API gateway""" batch = BatchTokenOperations() return await batch.batch_validate_tokens(tokens) # ИспользованиС async def api_gateway_auth(request_tokens: list[str]): """ΠŸΡ€ΠΈΠΌΠ΅Ρ€ использования Π² API Gateway""" results = await validate_multiple_tokens(request_tokens) authenticated_requests = [] for token, is_valid in results.items(): if is_valid: # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π΄Π°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ для Π²Π°Π»ΠΈΠ΄Π½Ρ‹Ρ… Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² sessions = SessionTokenManager() payload = await sessions.verify_session(token) if payload: authenticated_requests.append({ "token": token, "user_id": payload.get("user_id"), "username": payload.get("username") }) return authenticated_requests ``` ### 3. ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ```python from auth.utils import get_user_data_by_token async def get_user_info(token: str) -> dict | None: """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ ΠΏΠΎ Ρ‚ΠΎΠΊΠ΅Π½Ρƒ""" try: user_data = await get_user_data_by_token(token) return user_data except Exception as e: print(f"Ошибка получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ: {e}") return None # ИспользованиС async def protected_endpoint(request): """ΠŸΡ€ΠΈΠΌΠ΅Ρ€ Π·Π°Ρ‰ΠΈΡ‰Π΅Π½Π½ΠΎΠ³ΠΎ endpoint Π² микросСрвисС""" token = await extract_token_from_request(request) user_info = await get_user_info(token) if not user_info: return {"error": "Unauthorized", "status": 401} return { "message": f"Hello, {user_info.get('username')}!", "user_id": user_info.get("user_id"), "status": 200 } ``` ## πŸ”§ HTTP Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ ΠΈ ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ### ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹ ```python from auth.utils import extract_token_from_request, get_safe_headers async def extract_auth_token(request) -> str | None: """Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ Ρ‚ΠΎΠΊΠ΅Π½Π° ΠΈΠ· Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… источников""" # 1. АвтоматичСскоС ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ (рСкомСндуСтся) token = await extract_token_from_request(request) if token: return token # 2. Π ΡƒΡ‡Π½ΠΎΠ΅ ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ΠΈΠ· Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² headers = get_safe_headers(request) # Bearer Ρ‚ΠΎΠΊΠ΅Π½ Π² Authorization auth_header = headers.get("authorization", "") if auth_header.startswith("Bearer "): return auth_header[7:].strip() # ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹ΠΉ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ X-Session-Token session_token = headers.get("x-session-token") if session_token: return session_token.strip() # Cookie (для Π²Π΅Π±-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ) if hasattr(request, "cookies"): cookie_token = request.cookies.get("session_token") if cookie_token: return cookie_token return None ``` ### ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ HTTP запросов ```bash # 1. Bearer Ρ‚ΠΎΠΊΠ΅Π½ Π² Authorization header curl -H "Authorization: Bearer your_jwt_token_here" \ http://localhost:8000/api/protected # 2. ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹ΠΉ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ curl -H "X-Session-Token: your_jwt_token_here" \ http://localhost:8000/api/protected # 3. Cookie (автоматичСски для Π²Π΅Π±-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ) curl -b "session_token=your_jwt_token_here" \ http://localhost:8000/api/protected ``` ## πŸ“Š ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ΠΈ статистика ### Health Check ```python from auth.tokens.monitoring import TokenMonitoring async def auth_health_check() -> dict: """Health check систСмы Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ""" monitoring = TokenMonitoring() try: # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ состояниС систСмы Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² health = await monitoring.health_check() # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ статистику stats = await monitoring.get_token_statistics() return { "status": health.get("status", "unknown"), "redis_connected": health.get("redis_connected", False), "active_sessions": stats.get("session_tokens", 0), "oauth_tokens": stats.get("oauth_access_tokens", 0) + stats.get("oauth_refresh_tokens", 0), "memory_usage_mb": stats.get("memory_usage", 0) / 1024 / 1024, "timestamp": int(time.time()) } except Exception as e: return { "status": "error", "error": str(e), "timestamp": int(time.time()) } # ИспользованиС Π² endpoint async def health_endpoint(): """Endpoint для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π°""" health_data = await auth_health_check() if health_data["status"] == "healthy": return {"health": health_data, "status": 200} else: return {"health": health_data, "status": 503} ``` ### Бтатистика использования ```python async def get_auth_statistics() -> dict: """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ статистику использования Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ""" monitoring = TokenMonitoring() stats = await monitoring.get_token_statistics() return { "sessions": { "active": stats.get("session_tokens", 0), "total_memory": stats.get("memory_usage", 0) }, "oauth": { "access_tokens": stats.get("oauth_access_tokens", 0), "refresh_tokens": stats.get("oauth_refresh_tokens", 0) }, "verification": { "pending": stats.get("verification_tokens", 0) }, "redis": { "connected": stats.get("redis_connected", False), "memory_usage_mb": stats.get("memory_usage", 0) / 1024 / 1024 } } ``` ## πŸ”’ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ для микросСрвисов ### Валидация Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ```python async def secure_token_validation(token: str) -> dict: """БСзопасная валидация Ρ‚ΠΎΠΊΠ΅Π½Π° с Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°ΠΌΠΈ""" if not token or len(token) < 10: return {"valid": False, "error": "Invalid token format"} try: sessions = SessionTokenManager() # 1. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ JWT структуру ΠΈ подпись payload = await sessions.verify_session(token) if not payload: return {"valid": False, "error": "Invalid JWT token"} user_id = payload.get("user_id") if not user_id: return {"valid": False, "error": "Missing user_id in token"} # 2. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ сущСствованиС сСссии Π² Redis session_exists = await redis.exists(f"session:{user_id}:{token}") if not session_exists: return {"valid": False, "error": "Session not found in Redis"} # 3. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ TTL ttl = await redis.ttl(f"session:{user_id}:{token}") if ttl <= 0: return {"valid": False, "error": "Session expired"} # 4. ОбновляСм last_activity await redis.hset(f"session:{user_id}:{token}", "last_activity", int(time.time())) return { "valid": True, "user_id": user_id, "username": payload.get("username"), "expires_in": ttl, "last_activity": int(time.time()) } except Exception as e: return {"valid": False, "error": f"Validation error: {str(e)}"} ``` ### Rate Limiting ```python from collections import defaultdict import time # ΠŸΡ€ΠΎΡΡ‚ΠΎΠΉ in-memory rate limiter (для production ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ Redis) request_counts = defaultdict(list) async def rate_limit_check(user_id: str, max_requests: int = 100, window_seconds: int = 60) -> bool: """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° rate limiting для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" current_time = time.time() user_requests = request_counts[user_id] # УдаляСм старыС запросы user_requests[:] = [req_time for req_time in user_requests if current_time - req_time < window_seconds] # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π»ΠΈΠΌΠΈΡ‚ if len(user_requests) >= max_requests: return False # ДобавляСм Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ запрос user_requests.append(current_time) return True # ИспользованиС Π² middleware async def auth_with_rate_limiting(request): """АутСнтификация с rate limiting""" auth_result = await authenticate_request(request) if auth_result["authenticated"]: user_id = str(auth_result["user_id"]) if not await rate_limit_check(user_id): return {"error": "Rate limit exceeded", "status": 429} return auth_result ``` ## πŸ§ͺ ВСстированиС ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ ### Unit тСсты ```python import pytest from unittest.mock import AsyncMock, patch @pytest.mark.asyncio async def test_microservice_auth(): """ВСст Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π² микросСрвисС""" # Mock request с Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠΌ mock_request = AsyncMock() mock_request.headers = {"authorization": "Bearer valid_token"} # Mock SessionTokenManager with patch('auth.tokens.sessions.SessionTokenManager') as mock_sessions: mock_sessions.return_value.verify_session.return_value = { "user_id": "123", "username": "testuser", "exp": int(time.time()) + 3600 } result = await authenticate_request(mock_request) assert result["authenticated"] is True assert result["user_id"] == "123" assert result["username"] == "testuser" @pytest.mark.asyncio async def test_batch_token_validation(): """ВСст массовой Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²""" tokens = ["token1", "token2", "token3"] with patch('auth.tokens.batch.BatchTokenOperations') as mock_batch: mock_batch.return_value.batch_validate_tokens.return_value = { "token1": True, "token2": False, "token3": True } results = await validate_multiple_tokens(tokens) assert results["token1"] is True assert results["token2"] is False assert results["token3"] is True ``` ### Integration тСсты ```python @pytest.mark.asyncio async def test_redis_integration(): """ВСст ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ с Redis""" from storage.redis import redis # ВСстируСм ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ping_result = await redis.ping() assert ping_result is True # ВСстируСм ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ с сСссиями test_key = "session:test:token123" test_data = {"user_id": "123", "username": "testuser"} # БохраняСм Π΄Π°Π½Π½Ρ‹Π΅ await redis.hset(test_key, mapping=test_data) await redis.expire(test_key, 3600) # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π΄Π°Π½Π½Ρ‹Π΅ stored_data = await redis.hgetall(test_key) assert stored_data[b"user_id"].decode() == "123" assert stored_data[b"username"].decode() == "testuser" # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ TTL ttl = await redis.ttl(test_key) assert ttl > 0 # ΠžΡ‡ΠΈΡ‰Π°Π΅ΠΌ await redis.delete(test_key) ``` ## πŸ“‹ Checklist для ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ ### ΠŸΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΊΠ° - [ ] НастроСн Redis connection pool с Ρ‚Π΅ΠΌΠΈ ΠΆΠ΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π°ΠΌΠΈ - [ ] УстановлСны зависимости: `auth.tokens.*`, `auth.utils` - [ ] НастроСны environment variables (JWT_SECRET, REDIS_URL) ### РСализация - [ ] Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π° функция извлСчСния Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ΠΈΠ· запросов - [ ] Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° сСссий Ρ‡Π΅Ρ€Π΅Π· SessionTokenManager - [ ] НастроСна ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ - [ ] Π”ΠΎΠ±Π°Π²Π»Π΅Π½ health check endpoint ### Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ - [ ] Валидация Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ Redis сСссий - [ ] НастроСн rate limiting (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ) - [ ] Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ событий Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ - [ ] ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΈΡΡ‚Π΅ΠΊΡˆΠΈΡ… Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ### ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ - [ ] Health check ΠΈΠ½Ρ‚Π΅Π³Ρ€ΠΈΡ€ΠΎΠ²Π°Π½ Π² систСму ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° - [ ] ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΡΠΎΠ±ΠΈΡ€Π°ΡŽΡ‚ΡΡ - [ ] АлСрты настроСны для ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌ с Redis/JWT ### ВСстированиС - [ ] Unit тСсты для Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ - [ ] Integration тСсты с Redis - [ ] E2E тСсты с Ρ€Π΅Π°Π»ΡŒΠ½Ρ‹ΠΌΠΈ Ρ‚ΠΎΠΊΠ΅Π½Π°ΠΌΠΈ - [ ] Load тСсты для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ