refactoring hexagonal and clean architecture
This commit is contained in:
@@ -22,7 +22,7 @@ pub struct AuthUser {
|
||||
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware.
|
||||
///
|
||||
/// Uso en handlers:
|
||||
/// ```rust
|
||||
/// ```ignore
|
||||
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -80,6 +80,9 @@ pub enum AuthError {
|
||||
|
||||
#[error("Acceso denegado: {0}")]
|
||||
AccessDenied(String),
|
||||
|
||||
#[error("Servicio de autenticación no disponible")]
|
||||
AuthServiceUnavailable,
|
||||
}
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
@@ -90,6 +93,7 @@ impl IntoResponse for AuthError {
|
||||
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()),
|
||||
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()),
|
||||
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
|
||||
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()),
|
||||
};
|
||||
|
||||
let body = axum::Json(serde_json::json!({
|
||||
@@ -100,146 +104,75 @@ impl IntoResponse for AuthError {
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware de autenticación simplificado - solo valida si existe un token
|
||||
/// Middleware de autenticación seguro.
|
||||
///
|
||||
/// Valida el token JWT contra el servicio de autenticación configurado.
|
||||
/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación.
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AuthError> {
|
||||
// Check URL for special no_validation parameter to break auth loops
|
||||
let uri = request.uri().to_string();
|
||||
let skip_validation = uri.contains("no_redirect=true") || uri.contains("bypass_auth=true");
|
||||
|
||||
if skip_validation {
|
||||
tracing::info!("Bypassing token validation due to special URL parameter");
|
||||
// Create a default user for the request
|
||||
let current_user = CurrentUser {
|
||||
id: "default-user-id".to_string(),
|
||||
username: "usuario".to_string(),
|
||||
email: "usuario@example.com".to_string(),
|
||||
role: "user".to_string(),
|
||||
};
|
||||
request.extensions_mut().insert(current_user);
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// En una primera etapa, simplemente verificar si hay un token, sin validarlo
|
||||
if let Some(token_str) = headers
|
||||
// Extraer el token Bearer del header Authorization
|
||||
let token_str = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.strip_prefix("Bearer ")) {
|
||||
|
||||
// Handle mock tokens differently
|
||||
let is_mock = token_str.contains("mock") || token_str == "mock_access_token";
|
||||
|
||||
if is_mock {
|
||||
tracing::info!("Mock token detected, using simplified validation");
|
||||
let current_user = CurrentUser {
|
||||
id: "test-user-id".to_string(),
|
||||
username: "test".to_string(),
|
||||
email: "test@example.com".to_string(),
|
||||
role: "user".to_string(),
|
||||
};
|
||||
request.extensions_mut().insert(current_user);
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// Process normal token - try to validate it using JWT service
|
||||
tracing::info!("Processing token: {}", token_str.chars().take(8).collect::<String>() + "...");
|
||||
|
||||
// Try to get the token service and validate the token
|
||||
if let Some(auth_service) = state.auth_service.as_ref() {
|
||||
let token_service = &auth_service.token_service;
|
||||
match token_service.validate_token(token_str) {
|
||||
Ok(claims) => {
|
||||
tracing::info!("Token validated successfully for user: {}", claims.username);
|
||||
let current_user = CurrentUser {
|
||||
id: claims.sub,
|
||||
username: claims.username,
|
||||
email: claims.email,
|
||||
role: claims.role,
|
||||
};
|
||||
request.extensions_mut().insert(current_user);
|
||||
return Ok(next.run(request).await);
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("Token validation failed: {}", e);
|
||||
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no auth service available, use token claims from parsing JWT manually
|
||||
// Try to decode the token manually using jsonwebtoken
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
|
||||
|
||||
// Try with default secret (from environment or config)
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "oxicloud_secret_key_please_change_in_production".to_string());
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
username: String,
|
||||
email: String,
|
||||
role: String,
|
||||
}
|
||||
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
match decode::<Claims>(
|
||||
token_str,
|
||||
&DecodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
&validation
|
||||
) {
|
||||
Ok(token_data) => {
|
||||
tracing::info!("Token decoded successfully for user: {}", token_data.claims.username);
|
||||
.and_then(|value| value.strip_prefix("Bearer "))
|
||||
.ok_or(AuthError::TokenNotProvided)?;
|
||||
|
||||
// Validar que el token no esté vacío
|
||||
let token_str = token_str.trim();
|
||||
if token_str.is_empty() {
|
||||
return Err(AuthError::TokenNotProvided);
|
||||
}
|
||||
|
||||
tracing::debug!("Processing authentication token");
|
||||
|
||||
// Validar el token usando el servicio de autenticación
|
||||
if let Some(auth_service) = state.auth_service.as_ref() {
|
||||
let token_service = &auth_service.token_service;
|
||||
match token_service.validate_token(token_str) {
|
||||
Ok(claims) => {
|
||||
tracing::debug!("Token validated successfully for user: {}", claims.username);
|
||||
let current_user = CurrentUser {
|
||||
id: token_data.claims.sub,
|
||||
username: token_data.claims.username,
|
||||
email: token_data.claims.email,
|
||||
role: token_data.claims.role,
|
||||
id: claims.sub,
|
||||
username: claims.username,
|
||||
email: claims.email,
|
||||
role: claims.role,
|
||||
};
|
||||
request.extensions_mut().insert(current_user);
|
||||
return Ok(next.run(request).await);
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("Fallback token decode failed: {}", e);
|
||||
tracing::warn!("Token validation failed: {}", e);
|
||||
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si hay un indicador para evitar redirección, permitir el acceso sin token
|
||||
if uri.contains("api/") && uri.contains("login") {
|
||||
tracing::info!("Allowing access to login endpoint without token");
|
||||
return Ok(next.run(request).await);
|
||||
}
|
||||
|
||||
// Si no hay token, devolver error de token no proporcionado
|
||||
Err(AuthError::TokenNotProvided)
|
||||
// Si no hay servicio de autenticación disponible, denegar acceso
|
||||
tracing::error!("Auth middleware invoked but auth service is not configured");
|
||||
Err(AuthError::AuthServiceUnavailable)
|
||||
}
|
||||
|
||||
// Middleware simplificado para verificar roles de administrador
|
||||
/// Middleware para verificar que el usuario autenticado tiene rol de administrador.
|
||||
///
|
||||
/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que
|
||||
/// `CurrentUser` esté presente en las extensiones de la request.
|
||||
pub async fn require_admin(
|
||||
headers: HeaderMap,
|
||||
mut request: Request,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Implementación simplificada que verifica si hay un token de admin
|
||||
if let Some(auth_value) = headers.get(header::AUTHORIZATION) {
|
||||
if let Ok(auth_str) = auth_value.to_str() {
|
||||
if auth_str.contains("admin") {
|
||||
// Autorizado como admin
|
||||
let current_user = CurrentUser {
|
||||
id: "admin-user-id".to_string(),
|
||||
username: "admin".to_string(),
|
||||
email: "admin@example.com".to_string(),
|
||||
role: "admin".to_string(),
|
||||
};
|
||||
request.extensions_mut().insert(current_user);
|
||||
return next.run(request).await;
|
||||
}
|
||||
// Obtener el CurrentUser insertado por auth_middleware
|
||||
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
|
||||
if current_user.role == "admin" {
|
||||
tracing::debug!("Admin access granted for user: {}", current_user.username);
|
||||
return next.run(request).await;
|
||||
}
|
||||
tracing::warn!("Admin access denied for user: {} (role: {})", current_user.username, current_user.role);
|
||||
} else {
|
||||
tracing::warn!("Admin check failed: no authenticated user in request");
|
||||
}
|
||||
|
||||
// Acceso denegado
|
||||
|
||||
@@ -373,7 +373,7 @@ where
|
||||
let future = self.inner.call(req);
|
||||
return Box::pin(async move {
|
||||
let response = future.await.map_err(|e| e.into())?;
|
||||
Ok(response_map_body(response))
|
||||
Ok(response_map_body(response).await)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ where
|
||||
|
||||
return Box::pin(async move {
|
||||
let response = future.await.map_err(|e| e.into())?;
|
||||
let response = response_map_body(response);
|
||||
let response = response_map_body(response).await;
|
||||
|
||||
// No cachear errores
|
||||
if !response.status().is_success() {
|
||||
@@ -455,19 +455,27 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Función auxiliar para convertir cualquier cuerpo en Body
|
||||
fn response_map_body<B>(response: Response<B>) -> Response<Body>
|
||||
// Función auxiliar para convertir cualquier cuerpo en Body preservando su contenido.
|
||||
// Anteriormente esta función descartaba el body con Body::empty(), causando
|
||||
// pérdida de datos en respuestas no cacheadas.
|
||||
async fn response_map_body<B>(response: Response<B>) -> Response<Body>
|
||||
where
|
||||
B: http_body::Body + Send + 'static,
|
||||
B::Data: Send + 'static,
|
||||
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
let (parts, _body) = response.into_parts();
|
||||
|
||||
// Create a simple empty body as a fallback - in production you would handle this better
|
||||
let mapped_body = Body::empty();
|
||||
|
||||
Response::from_parts(parts, mapped_body)
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
let (parts, body) = response.into_parts();
|
||||
|
||||
// Collect the full body into Bytes, preserving all response data
|
||||
let collected = body
|
||||
.collect()
|
||||
.await
|
||||
.map(|c| c.to_bytes())
|
||||
.unwrap_or_default();
|
||||
|
||||
Response::from_parts(parts, Body::from(collected))
|
||||
}
|
||||
|
||||
/// Inicia una tarea de limpieza periódica para el caché
|
||||
@@ -488,14 +496,9 @@ pub fn start_cache_cleanup_task(cache: HttpCache) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hyper::{Request, Body, Response};
|
||||
use axum::routing::get;
|
||||
use axum::{Extension, Json, Router};
|
||||
use tower::ServiceExt;
|
||||
use http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Hash)]
|
||||
struct TestData {
|
||||
id: u32,
|
||||
name: String,
|
||||
@@ -505,13 +508,13 @@ mod tests {
|
||||
async fn test_etag_generation() {
|
||||
let cache = HttpCache::new();
|
||||
|
||||
let data1 = TestData { id: 1, name: "Test".to_string() };
|
||||
let data2 = TestData { id: 1, name: "Test".to_string() };
|
||||
let data3 = TestData { id: 2, name: "Test".to_string() };
|
||||
let data1 = serde_json::to_vec(&TestData { id: 1, name: "Test".to_string() }).unwrap();
|
||||
let data2 = serde_json::to_vec(&TestData { id: 1, name: "Test".to_string() }).unwrap();
|
||||
let data3 = serde_json::to_vec(&TestData { id: 2, name: "Test".to_string() }).unwrap();
|
||||
|
||||
let etag1 = cache.calculate_etag(&data1);
|
||||
let etag2 = cache.calculate_etag(&data2);
|
||||
let etag3 = cache.calculate_etag(&data3);
|
||||
let etag1 = cache.calculate_etag_for_bytes(&data1);
|
||||
let etag2 = cache.calculate_etag_for_bytes(&data2);
|
||||
let etag3 = cache.calculate_etag_for_bytes(&data3);
|
||||
|
||||
// Mismos datos deben generar mismo ETag
|
||||
assert_eq!(etag1, etag2);
|
||||
@@ -524,17 +527,12 @@ mod tests {
|
||||
async fn test_cache_hit_miss() {
|
||||
let cache = HttpCache::new();
|
||||
|
||||
// Primera petición (cache miss)
|
||||
let response1 = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::from(r#"{"id":1,"name":"Test"}"#))
|
||||
.unwrap();
|
||||
|
||||
let (parts1, body1) = response1.into_parts();
|
||||
let bytes1 = hyper::body::to_bytes(body1).await.unwrap();
|
||||
// Crear datos de prueba directamente como Bytes
|
||||
let bytes1 = Bytes::from(r#"{"id":1,"name":"Test"}"#);
|
||||
let headers1 = HeaderMap::new();
|
||||
|
||||
let etag1 = cache.calculate_etag_for_bytes(&bytes1);
|
||||
cache.set("test", etag1.clone(), Some(bytes1.clone()), parts1.headers.clone(), None);
|
||||
cache.set("test", etag1.clone(), Some(bytes1.clone()), headers1, None);
|
||||
|
||||
// Verificar cache hit
|
||||
let entry = cache.get("test").unwrap();
|
||||
|
||||
Reference in New Issue
Block a user