2024-08-31 03:32:37 +03:00
|
|
|
|
use actix_web::error::ErrorInternalServerError;
|
|
|
|
|
|
use redis::{aio::MultiplexedConnection, AsyncCommands};
|
2025-09-01 20:36:15 +03:00
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
use std::{collections::HashMap, env, error::Error};
|
|
|
|
|
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
|
|
|
|
|
use log::{info, warn};
|
2024-08-31 03:32:37 +03:00
|
|
|
|
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
|
|
|
|
|
|
use reqwest::Client as HTTPClient;
|
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
2025-09-01 20:36:15 +03:00
|
|
|
|
// Старые структуры для совместимости с get_id_by_token
|
2024-08-31 03:32:37 +03:00
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct AuthResponse {
|
|
|
|
|
|
data: Option<AuthData>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct AuthData {
|
|
|
|
|
|
validate_jwt_token: Option<ValidateJWTToken>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct ValidateJWTToken {
|
|
|
|
|
|
is_valid: bool,
|
|
|
|
|
|
claims: Option<Claims>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct Claims {
|
|
|
|
|
|
sub: Option<String>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-01 20:36:15 +03:00
|
|
|
|
// Структуры для JWT токенов
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
|
struct TokenClaims {
|
|
|
|
|
|
user_id: String,
|
|
|
|
|
|
username: Option<String>,
|
|
|
|
|
|
exp: Option<usize>,
|
|
|
|
|
|
iat: Option<usize>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Структура для данных пользователя из Redis сессии
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
|
|
|
|
|
pub struct Author {
|
|
|
|
|
|
pub user_id: String,
|
|
|
|
|
|
pub username: Option<String>,
|
|
|
|
|
|
pub token_type: Option<String>,
|
|
|
|
|
|
pub created_at: Option<String>,
|
|
|
|
|
|
pub last_activity: Option<String>,
|
|
|
|
|
|
pub auth_data: Option<String>,
|
|
|
|
|
|
pub device_info: Option<String>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-31 03:32:37 +03:00
|
|
|
|
/// Получает айди пользователя из токена в заголовке
|
|
|
|
|
|
pub async fn get_id_by_token(token: &str) -> Result<String, Box<dyn Error>> {
|
2025-08-02 00:18:09 +03:00
|
|
|
|
let auth_api_base = env::var("CORE_URL")?;
|
2024-08-31 03:32:37 +03:00
|
|
|
|
let query_name = "validate_jwt_token";
|
|
|
|
|
|
let operation = "ValidateToken";
|
|
|
|
|
|
let mut headers = HeaderMap::new();
|
|
|
|
|
|
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
|
|
|
|
|
|
|
|
|
|
|
let mut variables = HashMap::<String, HashMap<String, String>>::new();
|
|
|
|
|
|
let mut params = HashMap::<String, String>::new();
|
|
|
|
|
|
params.insert("token".to_string(), token.to_string());
|
|
|
|
|
|
params.insert("token_type".to_string(), "access_token".to_string());
|
|
|
|
|
|
variables.insert("params".to_string(), params);
|
|
|
|
|
|
|
|
|
|
|
|
let gql = json!({
|
|
|
|
|
|
"query": format!("query {}($params: ValidateJWTTokenInput!) {{ {}(params: $params) {{ is_valid claims }} }}", operation, query_name),
|
|
|
|
|
|
"operationName": operation,
|
|
|
|
|
|
"variables": variables
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let client = HTTPClient::new();
|
|
|
|
|
|
let response = client
|
|
|
|
|
|
.post(&auth_api_base)
|
|
|
|
|
|
.headers(headers)
|
|
|
|
|
|
.json(&gql)
|
|
|
|
|
|
.send()
|
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
|
|
if response.status().is_success() {
|
|
|
|
|
|
let auth_response: AuthResponse = response.json().await?;
|
|
|
|
|
|
if let Some(auth_data) = auth_response.data {
|
|
|
|
|
|
if let Some(validate_jwt_token) = auth_data.validate_jwt_token {
|
|
|
|
|
|
if validate_jwt_token.is_valid {
|
|
|
|
|
|
if let Some(claims) = validate_jwt_token.claims {
|
|
|
|
|
|
if let Some(sub) = claims.sub {
|
|
|
|
|
|
return Ok(sub);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-12 14:13:35 +03:00
|
|
|
|
Err(Box::new(std::io::Error::other("Invalid token response")))
|
2024-08-31 03:32:37 +03:00
|
|
|
|
} else {
|
2025-08-12 14:13:35 +03:00
|
|
|
|
Err(Box::new(std::io::Error::other(format!(
|
|
|
|
|
|
"Request failed with status: {}",
|
|
|
|
|
|
response.status()
|
|
|
|
|
|
))))
|
2024-08-31 03:32:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-01 20:36:15 +03:00
|
|
|
|
/// Декодирует JWT токен и извлекает claims с проверкой истечения
|
|
|
|
|
|
fn decode_jwt_token(token: &str) -> Result<TokenClaims, Box<dyn Error>> {
|
|
|
|
|
|
// В реальном приложении здесь должен быть настоящий секретный ключ
|
|
|
|
|
|
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::<TokenClaims>(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))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Быстро извлекает user_id из JWT токена для работы с квотами
|
|
|
|
|
|
pub fn extract_user_id_from_token(token: &str) -> Result<String, Box<dyn Error>> {
|
|
|
|
|
|
let claims = decode_jwt_token(token)?;
|
|
|
|
|
|
Ok(claims.user_id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Проверяет валидность JWT токена (включая истечение срока действия)
|
|
|
|
|
|
pub fn validate_token(token: &str) -> Result<bool, Box<dyn Error>> {
|
|
|
|
|
|
match decode_jwt_token(token) {
|
|
|
|
|
|
Ok(_) => Ok(true),
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
warn!("Token validation failed: {}", e);
|
|
|
|
|
|
Ok(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Получает user_id из JWT токена и базовые данные пользователя
|
|
|
|
|
|
pub async fn get_user_by_token(
|
|
|
|
|
|
token: &str,
|
|
|
|
|
|
redis: &mut MultiplexedConnection,
|
|
|
|
|
|
) -> Result<Author, Box<dyn Error>> {
|
|
|
|
|
|
// Декодируем JWT токен для получения user_id
|
|
|
|
|
|
let claims = decode_jwt_token(token)?;
|
|
|
|
|
|
let user_id = &claims.user_id;
|
|
|
|
|
|
|
|
|
|
|
|
info!("Extracted user_id from JWT token: {}", user_id);
|
|
|
|
|
|
|
|
|
|
|
|
// Проверяем валидность токена через сессию в Redis (опционально)
|
|
|
|
|
|
let token_key = format!("session:{}:{}", user_id, token);
|
|
|
|
|
|
let session_exists: bool = redis
|
|
|
|
|
|
.exists(&token_key)
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
|
warn!("Failed to check session existence in Redis: {}", e);
|
|
|
|
|
|
// Не критичная ошибка, продолжаем с базовыми данными
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
|
|
if session_exists {
|
|
|
|
|
|
// Обновляем last_activity если сессия существует
|
|
|
|
|
|
let current_time = std::time::SystemTime::now()
|
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
|
.unwrap()
|
|
|
|
|
|
.as_secs();
|
|
|
|
|
|
|
|
|
|
|
|
let _: () = redis
|
|
|
|
|
|
.hset(&token_key, "last_activity", current_time.to_string())
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
|
warn!("Failed to update last_activity: {}", e);
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or(());
|
|
|
|
|
|
|
|
|
|
|
|
info!("Updated last_activity for session: {}", token_key);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
info!("Session not found in Redis, proceeding with JWT-only data");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Создаем базовый объект Author с данными из JWT
|
|
|
|
|
|
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: None,
|
|
|
|
|
|
device_info: None,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
info!("Successfully created author data for user_id: {}", user_id);
|
|
|
|
|
|
Ok(author)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-31 03:32:37 +03:00
|
|
|
|
/// Сохраняет имя файла в Redis для пользователя
|
|
|
|
|
|
pub async fn user_added_file(
|
|
|
|
|
|
redis: &mut MultiplexedConnection,
|
|
|
|
|
|
user_id: &str,
|
2024-10-22 13:15:37 +03:00
|
|
|
|
filename: &str,
|
2024-08-31 03:32:37 +03:00
|
|
|
|
) -> Result<(), actix_web::Error> {
|
|
|
|
|
|
redis
|
2024-10-22 13:15:37 +03:00
|
|
|
|
.sadd::<&str, &str, ()>(user_id, filename)
|
2024-08-31 03:32:37 +03:00
|
|
|
|
.await
|
2024-10-22 13:15:37 +03:00
|
|
|
|
.map_err(|_| ErrorInternalServerError(format!("Failed to save {} in Redis", filename)))?; // Добавляем имя файла в набор пользователя
|
2024-08-31 03:32:37 +03:00
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|