# πŸ”‘ Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ сСссиями ## 🎯 ΠžΠ±Π·ΠΎΡ€ БистСма управлСния сСссиями Π½Π° основС JWT Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² с Redis Ρ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ΠΌ для ΠΎΡ‚Π·Ρ‹Π²Π° ΠΈ ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° активности. ## πŸ—οΈ АрхитСктура ### ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏ Ρ€Π°Π±ΠΎΡ‚Ρ‹ 1. **JWT Ρ‚ΠΎΠΊΠ΅Π½Ρ‹** с payload `{user_id, username, iat, exp}` 2. **Redis Ρ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅** для ΠΎΡ‚Π·Ρ‹Π²Π° ΠΈ управлСния ΠΆΠΈΠ·Π½Π΅Π½Π½Ρ‹ΠΌ Ρ†ΠΈΠΊΠ»ΠΎΠΌ 3. **ΠœΠ½ΠΎΠΆΠ΅ΡΡ‚Π²Π΅Π½Π½Ρ‹Π΅ сСссии** Π½Π° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ 4. **АвтоматичСскоС ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅** `last_activity` ΠΏΡ€ΠΈ активности ### Redis структура ```bash session:{user_id}:{token} # Hash: {user_id, username, device_info, last_activity} user_sessions:{user_id} # Set: {token1, token2, ...} ``` ### Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ Ρ‚ΠΎΠΊΠ΅Π½Π° (ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚) 1. Cookie `session_token` (httpOnly) 2. Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ `Authorization: Bearer ` 3. Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ `X-Session-Token` 4. `scope["auth_token"]` (Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠΉ) ## πŸ”§ SessionTokenManager ### ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ ```python from auth.tokens.sessions import SessionTokenManager sessions = SessionTokenManager() # Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ сСссии token = await sessions.create_session( user_id="123", auth_data={"provider": "local"}, username="john_doe", device_info={"ip": "192.168.1.1", "user_agent": "Mozilla/5.0"} ) # Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ JWT Ρ‚ΠΎΠΊΠ΅Π½Π° сСссии token = await sessions.create_session_token( user_id="123", token_data={"username": "john_doe", "device_info": "..."} ) # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° сСссии payload = await sessions.verify_session(token) # Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚: {"user_id": "123", "username": "john_doe", "iat": 1640995200, "exp": 1643587200} # Валидация Ρ‚ΠΎΠΊΠ΅Π½Π° сСссии valid, data = await sessions.validate_session_token(token) # ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… сСссии session_data = await sessions.get_session_data(token, user_id) # ОбновлСниС сСссии new_token = await sessions.refresh_session(user_id, old_token, device_info) # ΠžΡ‚Π·Ρ‹Π² сСссии await sessions.revoke_session_token(token) # ΠžΡ‚Π·Ρ‹Π² всСх сСссий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ revoked_count = await sessions.revoke_user_sessions(user_id) # ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ всСх сСссий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ user_sessions = await sessions.get_user_sessions(user_id) ``` ## πŸͺ httpOnly Cookies ### ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏΡ‹ Ρ€Π°Π±ΠΎΡ‚Ρ‹ 1. **БСзопасноС Ρ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅**: Π’ΠΎΠΊΠ΅Π½Ρ‹ сСссий хранятся Π² httpOnly cookies, нСдоступных для JavaScript 2. **АвтоматичСская ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ°**: Cookies автоматичСски ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ с ΠΊΠ°ΠΆΠ΄Ρ‹ΠΌ запросом 3. **Π—Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ XSS**: httpOnly cookies Π·Π°Ρ‰ΠΈΡ‰Π΅Π½Ρ‹ ΠΎΡ‚ ΠΊΡ€Π°ΠΆΠΈ Ρ‡Π΅Ρ€Π΅Π· JavaScript 4. **Двойная ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ°**: БистСма ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ ΠΊΠ°ΠΊ cookies, Ρ‚Π°ΠΊ ΠΈ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Authorization ### ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ cookies ```python # settings.py SESSION_COOKIE_NAME = "session_token" SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SECURE = True # для HTTPS SESSION_COOKIE_SAMESITE = "lax" SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 Π΄Π½Π΅ΠΉ ``` ### Установка cookies ```python # Π’ AuthMiddleware def set_session_cookie(self, response: Response, token: str) -> None: """УстанавливаСт httpOnly cookie с Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠΌ сСссии""" response.set_cookie( key=SESSION_COOKIE_NAME, value=token, httponly=SESSION_COOKIE_HTTPONLY, secure=SESSION_COOKIE_SECURE, samesite=SESSION_COOKIE_SAMESITE, max_age=SESSION_COOKIE_MAX_AGE ) ``` ## πŸ” Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² ### АвтоматичСскоС ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ```python from auth.utils import extract_token_from_request, get_auth_token, get_safe_headers # ΠŸΡ€ΠΎΡΡ‚ΠΎΠ΅ ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ΠΈΠ· cookies/headers token = await extract_token_from_request(request) # Π Π°ΡΡˆΠΈΡ€Π΅Π½Π½ΠΎΠ΅ ΠΈΠ·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ с Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ token = await get_auth_token(request) # Ручная ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° источников headers = get_safe_headers(request) token = headers.get("authorization", "").replace("Bearer ", "") # Π˜Π·Π²Π»Π΅Ρ‡Π΅Π½ΠΈΠ΅ ΠΈΠ· GraphQL контСкста from auth.utils import get_auth_token_from_context token = await get_auth_token_from_context(info) ``` ### ΠŸΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚ источников БистСма провСряСт Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ Π² ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌ порядкС ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚Π°: 1. **httpOnly cookies** - основной источник для Π²Π΅Π±-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ 2. **Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Authorization** - для API ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΠΎΠ² ΠΈ ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ… ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ ```python # auth/utils.py async def extract_token_from_request(request) -> str | None: """DRY функция для извлСчСния Ρ‚ΠΎΠΊΠ΅Π½Π° ΠΈΠ· request""" # 1. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ cookies if hasattr(request, "cookies") and request.cookies: token = request.cookies.get(SESSION_COOKIE_NAME) if token: return token # 2. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Authorization headers = get_safe_headers(request) auth_header = headers.get("authorization", "") if auth_header and auth_header.startswith("Bearer "): token = auth_header[7:].strip() return token return None ``` ### БСзопасноС ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΎΠ² ```python # auth/utils.py def get_safe_headers(request: Any) -> dict[str, str]: """БСзопасно ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠΈ запроса""" headers = {} try: # ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚: scope ΠΈΠ· ASGI if hasattr(request, "scope") and isinstance(request.scope, dict): scope_headers = request.scope.get("headers", []) if scope_headers: headers.update({k.decode("utf-8").lower(): v.decode("utf-8") for k, v in scope_headers}) # Π’Ρ‚ΠΎΡ€ΠΎΠΉ ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚: ΠΌΠ΅Ρ‚ΠΎΠ΄ headers() ΠΈΠ»ΠΈ Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚ headers if hasattr(request, "headers"): if callable(request.headers): h = request.headers() if h: headers.update({k.lower(): v for k, v in h.items()}) else: h = request.headers if hasattr(h, "items") and callable(h.items): headers.update({k.lower(): v for k, v in h.items()}) except Exception as e: logger.warning(f"Ошибка ΠΏΡ€ΠΈ доступС ΠΊ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΊΠ°ΠΌ: {e}") return headers ``` ## πŸ”„ Π–ΠΈΠ·Π½Π΅Π½Π½Ρ‹ΠΉ Ρ†ΠΈΠΊΠ» сСссии ### Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ сСссии ```python # auth/tokens/sessions.py async def create_session(author_id: int, email: str, **kwargs) -> str: """Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ Π½ΠΎΠ²ΡƒΡŽ сСссию для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" session_data = { "author_id": author_id, "email": email, "created_at": int(time.time()), **kwargs } # Π“Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ‚ΠΎΠΊΠ΅Π½ token = generate_session_token() # БохраняСм Π² Redis await redis.execute( "SETEX", f"session:{token}", SESSION_TOKEN_LIFE_SPAN, json.dumps(session_data) ) return token ``` ### ВСрификация сСссии ```python # auth/tokens/storage.py async def verify_session(token: str) -> dict | None: """Π’Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΡƒΠ΅Ρ‚ Ρ‚ΠΎΠΊΠ΅Π½ сСссии""" if not token: return None try: # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π΄Π°Π½Π½Ρ‹Π΅ сСссии ΠΈΠ· Redis session_data = await redis.execute("GET", f"session:{token}") if not session_data: return None return json.loads(session_data) except Exception as e: logger.error(f"Ошибка Π²Π΅Ρ€ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ сСссии: {e}") return None ``` ### ОбновлСниС сСссии ```python async def refresh_session(user_id: str, old_token: str, device_info: dict = None) -> str: """ΠžΠ±Π½ΠΎΠ²Π»ΡΠ΅Ρ‚ сСссию ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΡΡ‚Π°Ρ€ΡƒΡŽ сСссию old_payload = await verify_session(old_token) if not old_payload: raise InvalidTokenError("Invalid session token") # ΠžΡ‚Π·Ρ‹Π²Π°Π΅ΠΌ старый Ρ‚ΠΎΠΊΠ΅Π½ await revoke_session_token(old_token) # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Π½ΠΎΠ²Ρ‹ΠΉ Ρ‚ΠΎΠΊΠ΅Π½ new_token = await create_session( user_id=user_id, username=old_payload.get("username"), device_info=device_info or old_payload.get("device_info", {}) ) return new_token ``` ### Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ сСссии ```python # auth/tokens/storage.py async def delete_session(token: str) -> bool: """УдаляСт сСссию ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" try: result = await redis.execute("DEL", f"session:{token}") return bool(result) except Exception as e: logger.error(f"Ошибка удалСния сСссии: {e}") return False ``` ## πŸ”’ Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ### JWT Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ - **Алгоритм**: HS256 - **Secret**: Из ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠΉ окруТСния JWT_SECRET_KEY - **Payload**: `{user_id, username, iat, exp}` - **Expiration**: 30 Π΄Π½Π΅ΠΉ (настраиваСтся) ### Redis security - **TTL** для всСх Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² - **АтомарныС ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ** Ρ‡Π΅Ρ€Π΅Π· pipelines - **SCAN** вмСсто KEYS для ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ - **Π’Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ** для критичСских ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ ### Π—Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ Π°Ρ‚Π°ΠΊ - **XSS**: httpOnly cookies нСдоступны для JavaScript - **CSRF**: SameSite cookies ΠΈ CSRF Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ - **Session Hijacking**: Secure cookies ΠΈ рСгулярная ротация Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² - **Brute Force**: ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ Π²Ρ…ΠΎΠ΄Π° ΠΈ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ΠΎΠ² ## πŸ“Š ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ сСссий ### Бтатистика ```python from auth.tokens.monitoring import TokenMonitoring monitoring = TokenMonitoring() # Бтатистика Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² stats = await monitoring.get_token_statistics() print(f"Active sessions: {stats['session_tokens']}") print(f"Memory usage: {stats['memory_usage']} bytes") # Health check health = await monitoring.health_check() if health["status"] == "healthy": print("Session system is healthy") ``` ### Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ событий ```python # auth/middleware.py def log_auth_event(event_type: str, user_id: int | None = None, success: bool = True, **kwargs): """Π›ΠΎΠ³ΠΈΡ€ΡƒΠ΅Ρ‚ события Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ""" logger.info( "auth_event", event_type=event_type, user_id=user_id, success=success, ip_address=kwargs.get('ip'), user_agent=kwargs.get('user_agent'), **kwargs ) ``` ### ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ```python # auth/middleware.py from prometheus_client import Counter, Histogram # Π‘Ρ‡Π΅Ρ‚Ρ‡ΠΈΠΊΠΈ login_attempts = Counter('auth_login_attempts_total', 'Number of login attempts', ['success']) session_creations = Counter('auth_sessions_created_total', 'Number of sessions created') session_deletions = Counter('auth_sessions_deleted_total', 'Number of sessions deleted') # Гистограммы auth_duration = Histogram('auth_operation_duration_seconds', 'Time spent on auth operations', ['operation']) ``` ## πŸ§ͺ ВСстированиС ### Unit тСсты ```python import pytest from httpx import AsyncClient @pytest.mark.asyncio async def test_login_success(client: AsyncClient): """ВСст ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ Π²Ρ…ΠΎΠ΄Π°""" response = await client.post("/auth/login", json={ "email": "test@example.com", "password": "password123" }) assert response.status_code == 200 data = response.json() assert data["success"] is True assert "token" in data # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ установку cookie cookies = response.cookies assert "session_token" in cookies @pytest.mark.asyncio async def test_protected_endpoint_with_cookie(client: AsyncClient): """ВСст Π·Π°Ρ‰ΠΈΡ‰Π΅Π½Π½ΠΎΠ³ΠΎ endpoint с cookie""" # Π‘Π½Π°Ρ‡Π°Π»Π° Π²Ρ…ΠΎΠ΄ΠΈΠΌ Π² систСму login_response = await client.post("/auth/login", json={ "email": "test@example.com", "password": "password123" }) # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ cookie session_cookie = login_response.cookies.get("session_token") # Π”Π΅Π»Π°Π΅ΠΌ запрос ΠΊ Π·Π°Ρ‰ΠΈΡ‰Π΅Π½Π½ΠΎΠΌΡƒ endpoint response = await client.get("/auth/session", cookies={ "session_token": session_cookie }) assert response.status_code == 200 data = response.json() assert data["user"]["email"] == "test@example.com" ``` ## πŸ’‘ ΠŸΡ€ΠΈΠΌΠ΅Ρ€Ρ‹ использования ### 1. Π’Ρ…ΠΎΠ΄ Π² систСму ```typescript // Frontend - React/SolidJS const handleLogin = async (email: string, password: string) => { try { const response = await fetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password }), credentials: 'include', // Π’Π°ΠΆΠ½ΠΎ для cookies }); if (response.ok) { const data = await response.json(); // Cookie автоматичСски установится Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ΠΎΠΌ // ΠŸΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ Π½Π° Π³Π»Π°Π²Π½ΡƒΡŽ страницу window.location.href = '/'; } else { const error = await response.json(); console.error('Login failed:', error.message); } } catch (error) { console.error('Login error:', error); } }; ``` ### 2. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠΈ ```typescript // Frontend - ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ сСссии const checkAuth = async () => { try { const response = await fetch('/auth/session', { credentials: 'include', }); if (response.ok) { const data = await response.json(); if (data.user) { // ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Π½ setUser(data.user); setIsAuthenticated(true); } } } catch (error) { console.error('Auth check failed:', error); } }; ``` ### 3. Π—Π°Ρ‰ΠΈΡ‰Π΅Π½Π½Ρ‹ΠΉ API endpoint ```python # Backend - Python from auth.decorators import login_required, require_permission @login_required @require_permission("shout:create") async def create_shout(info, input_data): """Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ ΠΏΡ€Π°Π²""" user = info.context.get('user') # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ shout = Shout( title=input_data['title'], content=input_data['content'], author_id=user.id ) db.add(shout) db.commit() return shout ``` ### 4. Π’Ρ‹Ρ…ΠΎΠ΄ ΠΈΠ· систСмы ```typescript // Frontend - Π²Ρ‹Ρ…ΠΎΠ΄ const handleLogout = async () => { try { await fetch('/auth/logout', { method: 'POST', credentials: 'include', }); // ΠžΡ‡ΠΈΡ‰Π°Π΅ΠΌ локальноС состояниС setUser(null); setIsAuthenticated(false); // ΠŸΠ΅Ρ€Π΅Π½Π°ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ Π½Π° страницу Π²Ρ…ΠΎΠ΄Π° window.location.href = '/login'; } catch (error) { console.error('Logout failed:', error); } }; ```