use actix_web::error::ErrorInternalServerError; use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use log::{info, warn}; use redis::{AsyncCommands, aio::MultiplexedConnection}; use serde::{Deserialize, Serialize}; use std::{error::Error, time::Duration}; // Структуры для JWT токенов #[derive(Debug, Deserialize)] struct TokenClaims { user_id: String, username: Option, exp: Option, iat: Option, } // Структура для данных пользователя из Redis сессии #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Author { pub user_id: String, pub username: Option, pub token_type: Option, pub created_at: Option, pub last_activity: Option, pub auth_data: Option, pub device_info: Option, } /// Декодирует JWT токен и извлекает claims с проверкой истечения fn decode_jwt_token(token: &str) -> Result> { // В реальном приложении здесь должен быть настоящий секретный ключ let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); let key = DecodingKey::from_secret(secret.as_ref()); let mut validation = Validation::new(Algorithm::HS256); validation.validate_exp = true; // Включаем проверку истечения срока действия match decode::(token, &key, &validation) { Ok(token_data) => { let claims = token_data.claims; // Дополнительная проверка exp если поле присутствует if let Some(exp) = claims.exp { let current_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() as usize; if exp < current_time { warn!("JWT token expired: exp={}, current={}", exp, current_time); return Err(Box::new(std::io::Error::other("Token expired"))); } info!("JWT token valid until: {} (current: {})", exp, current_time); } info!( "Successfully decoded and validated JWT token for user: {}", claims.user_id ); Ok(claims) } Err(e) => { warn!("Failed to decode JWT token: {}", e); Err(Box::new(e)) } } } /// Извлекает токен из HTTP запроса (поддерживает Bearer, X-Session-Token, Cookie) pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option { // 1. Bearer токен в Authorization header if let Some(auth_header) = req.headers().get("authorization") { if let Ok(auth_str) = auth_header.to_str() { if let Some(stripped) = auth_str.strip_prefix("Bearer ") { return Some(stripped.trim().to_string()); } } } // 2. Кастомный заголовок X-Session-Token if let Some(session_token) = req.headers().get("x-session-token") { if let Ok(token_str) = session_token.to_str() { return Some(token_str.trim().to_string()); } } // 3. Cookie session_token (для веб-приложений) if let Some(cookie_header) = req.headers().get("cookie") { if let Ok(cookie_str) = cookie_header.to_str() { for cookie in cookie_str.split(';') { let cookie = cookie.trim(); if let Some(stripped) = cookie.strip_prefix("session_token=") { return Some(stripped.to_string()); } } } } None } /// Безопасная валидация токена с проверкой Redis сессий pub async fn secure_token_validation( token: &str, mut redis: Option<&mut MultiplexedConnection>, timeout: Duration, ) -> Result> { // Базовая проверка формата токена if token.is_empty() || token.len() < 10 { return Err(Box::new(std::io::Error::other("Invalid token format"))); } // 1. Проверяем JWT структуру и подпись let claims = decode_jwt_token(token)?; let user_id = &claims.user_id; info!("JWT token validated for user: {}", user_id); // 2. Проверяем существование сессии в Redis let session_exists = if let Some(ref mut redis) = redis { let session_key = format!("session:{}:{}", user_id, token); match tokio::time::timeout(timeout, redis.exists(&session_key)).await { Ok(Ok(exists)) => exists, Ok(Err(e)) => { warn!("Redis error checking session: {}", e); false } Err(_) => { warn!("Redis timeout checking session"); false } } } else { warn!("⚠️ Redis not available, skipping session validation"); false }; if !session_exists { info!("Session not found in Redis for user: {}", user_id); // В соответствии с руководством, можем продолжить с JWT-only данными // или вернуть ошибку в зависимости от политики безопасности } // 3. Проверяем TTL сессии если она существует let ttl = if session_exists && redis.is_some() { let session_key = format!("session:{}:{}", user_id, token); if let Some(ref mut redis) = redis { match tokio::time::timeout(timeout, redis.ttl(&session_key)).await { Ok(Ok(ttl_value)) => { if ttl_value <= 0 { return Err(Box::new(std::io::Error::other("Session expired"))); } ttl_value } Ok(Err(e)) => { warn!("Redis error getting TTL: {}", e); -1 } Err(_) => { warn!("Redis timeout getting TTL"); -1 } } } else { -1 } } else { -1 }; // 4. Обновляем last_activity если сессия активна if session_exists && redis.is_some() { let current_time = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); let session_key = format!("session:{}:{}", user_id, token); if let Some(redis) = redis { let _: Result<(), _> = tokio::time::timeout( timeout, redis.hset(&session_key, "last_activity", current_time.to_string()), ) .await .map_err(|_| warn!("Redis timeout updating last_activity")) .unwrap_or(Ok(())) .map_err(|e| warn!("Failed to update last_activity: {}", e)); } info!("Updated last_activity for session: {}", user_id); } // Создаем объект Author с расширенными данными let author = Author { user_id: user_id.clone(), username: claims.username.clone(), token_type: Some("jwt".to_string()), created_at: claims.iat.map(|ts| ts.to_string()), last_activity: Some( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() .to_string(), ), auth_data: if session_exists { Some(format!("redis_session_ttl:{}", ttl)) } else { Some("jwt_only".to_string()) }, device_info: None, }; info!( "Successfully validated token for user: {} (session_exists: {})", user_id, session_exists ); Ok(author) } /// Универсальная функция аутентификации для всех handlers /// Извлекает токен из запроса и выполняет полную валидацию pub async fn authenticate_request( req: &actix_web::HttpRequest, redis: Option<&mut MultiplexedConnection>, timeout: Duration, ) -> Result { // Извлекаем токен из запроса (поддерживает Bearer, X-Session-Token, Cookie) let token = extract_token_from_request(req).ok_or_else(|| { warn!("No authorization token provided"); actix_web::error::ErrorUnauthorized("Authorization token required") })?; // Безопасная валидация токена с проверкой Redis сессий secure_token_validation(&token, redis, timeout) .await .map_err(|e| { warn!("Token validation failed: {}", e); actix_web::error::ErrorUnauthorized("Invalid or expired token") }) } /// Универсальная функция аутентификации с использованием connection pool /// Автоматически получает и возвращает соединения из пула pub async fn authenticate_request_with_pool( req: &actix_web::HttpRequest, app_state: &crate::app_state::AppState, ) -> Result { // Извлекаем токен из запроса let token = extract_token_from_request(req).ok_or_else(|| { warn!("No authorization token provided"); actix_web::error::ErrorUnauthorized("Authorization token required") })?; // Получаем соединение из пула match app_state.get_redis_connection().await { Ok(mut conn) => { // Валидируем токен с Redis соединением let result = secure_token_validation(&token, Some(&mut conn), app_state.request_timeout).await; // Возвращаем соединение в пул app_state.return_redis_connection(conn).await; result.map_err(|e| { warn!("Token validation failed: {}", e); actix_web::error::ErrorUnauthorized("Invalid or expired token") }) } Err(_) => { // Fallback на JWT-only валидацию если Redis недоступен warn!("Redis pool unavailable, falling back to JWT-only validation"); secure_token_validation(&token, None, app_state.request_timeout) .await .map_err(|e| { warn!("Token validation failed: {}", e); actix_web::error::ErrorUnauthorized("Invalid or expired token") }) } } } /// Сохраняет имя файла в Redis для пользователя pub async fn user_added_file( redis: Option<&mut MultiplexedConnection>, user_id: &str, filename: &str, ) -> Result<(), actix_web::Error> { let Some(redis) = redis else { log::warn!( "⚠️ Redis not available, skipping file tracking for user {}", user_id ); return Ok(()); }; redis .sadd::<&str, &str, ()>(user_id, filename) .await .map_err(|_| ErrorInternalServerError(format!("Failed to save {} in Redis", filename)))?; // Добавляем имя файла в набор пользователя Ok(()) }