chore: translate all Spanish comments and log messages to English

This commit is contained in:
Dionisio
2026-02-12 09:41:25 +01:00
parent bc169de8f6
commit d31a413e57
89 changed files with 2480 additions and 2482 deletions
+29 -29
View File
@@ -15,7 +15,7 @@ use crate::application::dtos::user_dto::{
use crate::interfaces::errors::AppError;
pub fn auth_routes() -> Router<Arc<AppState>> {
// Rutas que NO requieren autenticación
// Routes that do NOT require authentication
let public_routes = Router::new()
.route("/register", post(register))
.route("/login", post(login))
@@ -27,14 +27,14 @@ pub fn auth_routes() -> Router<Arc<AppState>> {
.route("/oidc/callback", get(oidc_callback))
.route("/oidc/exchange", post(oidc_exchange));
// Rutas que SÍ requieren autenticación - usamos route_layer para aplicar middleware
// El middleware usará el state que se pase con .with_state() desde main.rs
// Routes that DO require authentication - we use route_layer to apply middleware
// The middleware will use the state passed with .with_state() from main.rs
let protected_routes = Router::new()
.route("/me", get(get_current_user))
.route("/change-password", put(change_password))
.route("/logout", post(logout));
// Combinar rutas públicas y protegidas
// Combine public and protected routes
public_routes.merge(protected_routes)
}
@@ -53,7 +53,7 @@ async fn register(
},
None => {
tracing::error!("Auth service not configured");
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
return Err(AppError::internal_error("Authentication service not configured"));
}
};
@@ -153,7 +153,7 @@ async fn login(
},
None => {
tracing::error!("Auth service not configured");
return Err(AppError::internal_error("Servicio de autenticación no configurado"));
return Err(AppError::internal_error("Authentication service not configured"));
}
};
@@ -174,7 +174,7 @@ async fn login(
// Ensure the response has the expected fields
if auth_response.access_token.is_empty() || auth_response.refresh_token.is_empty() {
tracing::error!("Login response contains empty tokens for user: {}", dto.username);
return Err(AppError::internal_error("Error generando tokens de autenticación"));
return Err(AppError::internal_error("Error generating authentication tokens"));
}
Ok((StatusCode::OK, Json(auth_response)))
@@ -198,7 +198,7 @@ async fn refresh_token(
// Normal process for real tokens
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
let auth_response = auth_service.auth_application_service.refresh_token(dto).await?;
@@ -214,37 +214,37 @@ async fn get_current_user(
) -> Result<impl IntoResponse, AppError> {
// Normal process for all users
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Extraer y validar el token directamente
// Extract and validate the token directly
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
// Validar el token y obtener claims
// Validate the token and get claims
let claims = auth_service.token_service.validate_token(token)
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
let user_id = claims.sub;
// Primero, actualizar las estadísticas de uso de almacenamiento
// IMPORTANTE: Esperamos el cálculo para devolver datos actualizados
// First, update the storage usage statistics
// IMPORTANT: We await the calculation to return updated data
if let Some(storage_usage_service) = state.storage_usage_service.as_ref() {
// Calcular storage de forma síncrona (esperamos el resultado)
// Calculate storage synchronously (we await the result)
match storage_usage_service.update_user_storage_usage(&user_id).await {
Ok(usage) => {
tracing::info!("Updated storage usage for user {}: {} bytes", user_id, usage);
},
Err(e) => {
// Solo log de warning, no fallar la petición completa
// Only log a warning, don't fail the entire request
tracing::warn!("Failed to update storage usage for user {}: {}", user_id, e);
}
}
}
// Ahora obtener los datos del usuario CON el almacenamiento actualizado
// Now get the user data WITH the updated storage
let user = auth_service.auth_application_service.get_user_by_id(&user_id).await?;
Ok((StatusCode::OK, Json(user)))
@@ -256,18 +256,18 @@ async fn change_password(
Json(dto): Json<ChangePasswordDto>,
) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Extraer y validar el token directamente
// Extract and validate the token directly
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
// Validar el token y obtener claims
// Validate the token and get claims
let claims = auth_service.token_service.validate_token(token)
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
auth_service.auth_application_service.change_password(&claims.sub, dto).await?;
@@ -279,18 +279,18 @@ async fn logout(
headers: HeaderMap,
) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Extraer y validar el token directamente
// Extract and validate the token directly
let token = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or_else(|| AppError::unauthorized("Token de autorización no encontrado"))?;
.ok_or_else(|| AppError::unauthorized("Authorization token not found"))?;
// Validar el token y obtener claims
// Validate the token and get claims
let claims = auth_service.token_service.validate_token(token)
.map_err(|e| AppError::unauthorized(&format!("Token inválido: {}", e)))?;
.map_err(|e| AppError::unauthorized(&format!("Invalid token: {}", e)))?;
// Use access token for logout (we don't have refresh token in headers)
auth_service.auth_application_service.logout(&claims.sub, token).await?;
@@ -314,7 +314,7 @@ async fn get_system_status(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
let auth_service = state.auth_service.as_ref()
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
.ok_or_else(|| AppError::internal_error("Authentication service not configured"))?;
// Count admin users to determine if system is initialized
let admin_count = auth_service.auth_application_service.count_admin_users().await
+84 -84
View File
@@ -13,86 +13,86 @@ use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;
use crate::interfaces::api::handlers::ApiResult;
/// Estado compartido para el handler de batch
/// Shared state for the batch handler
#[derive(Clone)]
pub struct BatchHandlerState {
pub batch_service: Arc<BatchOperationService>,
}
/// DTO para las solicitudes de operaciones en lote de archivos
/// DTO for batch file operation requests
#[derive(Debug, Deserialize)]
pub struct BatchFileOperationRequest {
/// IDs de los archivos a procesar
/// IDs of the files to process
pub file_ids: Vec<String>,
/// ID de la carpeta destino (opcional)
/// Target folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>,
}
/// DTO para las solicitudes de operaciones en lote de carpetas
/// DTO for batch folder operation requests
#[derive(Debug, Deserialize)]
pub struct BatchFolderOperationRequest {
/// IDs de las carpetas a procesar
/// IDs of the folders to process
pub folder_ids: Vec<String>,
/// Si la operación debe ser recursiva
/// Whether the operation should be recursive
#[serde(default)]
pub recursive: bool,
/// ID de la carpeta destino (opcional)
/// Target folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub target_folder_id: Option<String>,
}
/// DTO para las solicitudes de creación en lote de carpetas
/// DTO for batch folder creation requests
#[derive(Debug, Deserialize)]
pub struct BatchCreateFoldersRequest {
/// Detalles de las carpetas a crear
/// Details of the folders to create
pub folders: Vec<CreateFolderDetail>,
}
/// Detalle para creación de una carpeta
/// Detail for folder creation
#[derive(Debug, Deserialize)]
pub struct CreateFolderDetail {
/// Nombre de la carpeta
/// Folder name
pub name: String,
/// ID de la carpeta padre (opcional)
/// Parent folder ID (optional)
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
/// DTO para los resultados de operaciones en lote
/// DTO for batch operation results
#[derive(Debug, Serialize)]
pub struct BatchOperationResponse<T> {
/// Entidades procesadas exitosamente
/// Successfully processed entities
pub successful: Vec<T>,
/// Operaciones fallidas con sus mensajes de error
/// Failed operations with their error messages
pub failed: Vec<FailedOperation>,
/// Estadísticas de la operación
/// Operation statistics
pub stats: BatchOperationStats,
}
/// Operación fallida en un lote
/// Failed operation in a batch
#[derive(Debug, Serialize)]
pub struct FailedOperation {
/// Identificador de la entidad que falló
/// Identifier of the entity that failed
pub id: String,
/// Mensaje de error
/// Error message
pub error: String,
}
/// Estadísticas de una operación por lotes
/// Statistics for a batch operation
#[derive(Debug, Serialize)]
pub struct BatchOperationStats {
/// Número total de operaciones
/// Total number of operations
pub total: usize,
/// Número de operaciones exitosas
/// Number of successful operations
pub successful: usize,
/// Número de operaciones fallidas
/// Number of failed operations
pub failed: usize,
/// Tiempo total de ejecución en milisegundos
/// Total execution time in milliseconds
pub execution_time_ms: u128,
}
/// Convierte BatchStats del dominio a DTO
/// Converts domain BatchStats to DTO
impl From<BatchStats> for BatchOperationStats {
fn from(stats: BatchStats) -> Self {
Self {
@@ -104,7 +104,7 @@ impl From<BatchStats> for BatchOperationStats {
}
}
/// Convierte BatchResult<T> del dominio a DTO
/// Converts domain BatchResult<T> to DTO
impl<T, U> From<BatchResult<T>> for BatchOperationResponse<U>
where
U: From<T>,
@@ -124,12 +124,12 @@ where
}
}
/// Handler para mover múltiples archivos en lote
/// Handler for moving multiple files in batch
pub async fn move_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -139,35 +139,35 @@ pub async fn move_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.move_files(request.file_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into();
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para copiar múltiples archivos en lote
/// Handler for copying multiple files in batch
pub async fn copy_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -177,35 +177,35 @@ pub async fn copy_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.copy_files(request.file_ids, request.target_folder_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into();
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para eliminar múltiples archivos en lote
/// Handler for deleting multiple files in batch
pub async fn delete_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -215,13 +215,13 @@ pub async fn delete_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.delete_files(request.file_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Crear respuesta personalizada para IDs de string
// Create custom response for string IDs
let response = BatchOperationResponse {
successful: result.successful,
failed: result.failed.into_iter()
@@ -230,26 +230,26 @@ pub async fn delete_files_batch(
stats: result.stats.into(),
};
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para eliminar múltiples carpetas en lote
/// Handler for deleting multiple folders in batch
pub async fn delete_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
// Verify there are folders to process
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -259,13 +259,13 @@ pub async fn delete_folders_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.delete_folders(request.folder_ids, request.recursive)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Crear respuesta personalizada para IDs de string
// Create custom response for string IDs
let response = BatchOperationResponse {
successful: result.successful,
failed: result.failed.into_iter()
@@ -274,26 +274,26 @@ pub async fn delete_folders_batch(
stats: result.stats.into(),
};
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para crear múltiples carpetas en lote
/// Handler for creating multiple folders in batch
pub async fn create_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchCreateFoldersRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
// Verify there are folders to process
if request.folders.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -303,41 +303,41 @@ pub async fn create_folders_batch(
).into_response());
}
// Transformar el formato para el servicio
// Transform the format for the service
let folders = request.folders
.into_iter()
.map(|detail| (detail.name, detail.parent_id))
.collect();
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.create_folders(folders)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<FolderDto> = result.into();
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::CREATED // Todas exitosas
StatusCode::CREATED // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para obtener múltiples archivos en lote
/// Handler for getting multiple files in batch
pub async fn get_files_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFileOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay archivos para procesar
// Verify there are files to process
if request.file_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -347,35 +347,35 @@ pub async fn get_files_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.get_multiple_files(request.file_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into();
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
}
/// Handler para obtener múltiples carpetas en lote
/// Handler for getting multiple folders in batch
pub async fn get_folders_batch(
State(state): State<BatchHandlerState>,
Json(request): Json<BatchFolderOperationRequest>,
) -> ApiResult<impl IntoResponse> {
// Verificar que hay carpetas para procesar
// Verify there are folders to process
if request.folder_ids.is_empty() {
return Ok((
StatusCode::BAD_REQUEST,
@@ -385,24 +385,24 @@ pub async fn get_folders_batch(
).into_response());
}
// Ejecutar operación de lote
// Execute batch operation
let result = state.batch_service
.get_multiple_folders(request.folder_ids)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Convertir resultado a DTO
// Convert result to DTO
let response: BatchOperationResponse<FolderDto> = result.into();
// Determinar código de estado basado en los resultados
// Determine status code based on results
let status_code = if response.stats.failed > 0 {
if response.stats.successful > 0 {
StatusCode::PARTIAL_CONTENT // Algunas operaciones exitosas, otras fallidas
StatusCode::PARTIAL_CONTENT // Some operations successful, others failed
} else {
StatusCode::BAD_REQUEST // Todas fallaron
StatusCode::BAD_REQUEST // All failed
}
} else {
StatusCode::OK // Todas exitosas
StatusCode::OK // All successful
};
Ok((status_code, Json(response)).into_response())
+24 -24
View File
@@ -11,14 +11,14 @@ use tracing::{error, info};
use crate::application::ports::recent_ports::RecentItemsUseCase;
use crate::interfaces::middleware::auth::AuthUser;
/// Parámetros de consulta para obtener elementos recientes
/// Query parameters for getting recent items
#[derive(Deserialize)]
pub struct GetRecentParams {
#[serde(default)]
limit: Option<i32>,
}
/// Obtener elementos recientes del usuario
/// Get user's recent items
pub async fn get_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -28,22 +28,22 @@ pub async fn get_recent_items(
match recent_service.get_recent_items(user_id, params.limit).await {
Ok(items) => {
info!("Recuperados {} elementos recientes para usuario", items.len());
info!("Retrieved {} recent items for user", items.len());
(StatusCode::OK, Json(items)).into_response()
},
Err(err) => {
error!("Error al recuperar elementos recientes: {}", err);
error!("Error retrieving recent items: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al recuperar elementos recientes: {}", err)
"error": format!("Failed to retrieve recent items: {}", err)
}))
).into_response()
}
}
}
/// Registrar acceso a un elemento
/// Record access to an item
pub async fn record_item_access(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -51,39 +51,39 @@ pub async fn record_item_access(
) -> impl IntoResponse {
let user_id = &auth_user.id;
// Validar tipo de elemento
// Validate item type
if item_type != "file" && item_type != "folder" {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "El tipo de elemento debe ser 'file' o 'folder'"
"error": "Item type must be 'file' or 'folder'"
}))
).into_response();
}
match recent_service.record_item_access(user_id, &item_id, &item_type).await {
Ok(_) => {
info!("Registrado acceso a {} '{}' en recientes", item_type, item_id);
info!("Recorded access to {} '{}' in recents", item_type, item_id);
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Acceso registrado correctamente"
"message": "Access recorded successfully"
}))
).into_response()
},
Err(err) => {
error!("Error al registrar acceso en recientes: {}", err);
error!("Error recording access in recents: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al registrar acceso: {}", err)
"error": format!("Failed to record access: {}", err)
}))
).into_response()
}
}
}
/// Eliminar un elemento de recientes
/// Remove an item from recents
pub async fn remove_from_recent(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -94,36 +94,36 @@ pub async fn remove_from_recent(
match recent_service.remove_from_recent(user_id, &item_id, &item_type).await {
Ok(removed) => {
if removed {
info!("Eliminado {} '{}' de recientes", item_type, item_id);
info!("Removed {} '{}' from recents", item_type, item_id);
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Elemento eliminado de recientes"
"message": "Item removed from recents"
}))
).into_response()
} else {
info!("Elemento {} '{}' no estaba en recientes", item_type, item_id);
info!("Item {} '{}' was not in recents", item_type, item_id);
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"message": "Elemento no estaba en recientes"
"message": "Item was not in recents"
}))
).into_response()
}
},
Err(err) => {
error!("Error al eliminar de recientes: {}", err);
error!("Error removing from recents: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al eliminar de recientes: {}", err)
"error": format!("Failed to remove from recents: {}", err)
}))
).into_response()
}
}
}
/// Limpiar todos los elementos recientes
/// Clear all recent items
pub async fn clear_recent_items(
State(recent_service): State<Arc<dyn RecentItemsUseCase>>,
auth_user: AuthUser,
@@ -132,20 +132,20 @@ pub async fn clear_recent_items(
match recent_service.clear_recent_items(user_id).await {
Ok(_) => {
info!("Limpiados todos los elementos recientes para usuario");
info!("Cleared all recent items for user");
(
StatusCode::OK,
Json(serde_json::json!({
"message": "Elementos recientes limpiados correctamente"
"message": "Recent items cleared successfully"
}))
).into_response()
},
Err(err) => {
error!("Error al limpiar elementos recientes: {}", err);
error!("Error clearing recent items: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Fallo al limpiar elementos recientes: {}", err)
"error": format!("Failed to clear recent items: {}", err)
}))
).into_response()
}
+51 -51
View File
@@ -10,34 +10,34 @@ use crate::application::dtos::search_dto::SearchCriteriaDto;
use crate::common::di::AppState;
/**
* Manejador para las operaciones de búsqueda a través de la API.
* Handler for search operations through the API.
*
* Este manejador expone endpoints relacionados con la funcionalidad de búsqueda,
* permitiendo a los usuarios buscar archivos y carpetas usando diversos criterios.
* This handler exposes endpoints related to search functionality,
* allowing users to search for files and folders using various criteria.
*/
pub struct SearchHandler;
impl SearchHandler {
/**
* Realiza una búsqueda basada en los criterios proporcionados como parámetros de consulta.
* Performs a search based on the criteria provided as query parameters.
*
* Este endpoint permite búsquedas simples directamente con parámetros URL.
* This endpoint allows simple searches directly with URL parameters.
*
* @param state Estado de la aplicación con servicios
* @param query_params Parámetros de búsqueda como query string
* @return Respuesta HTTP con los resultados de la búsqueda
* @param state Application state with services
* @param query_params Search parameters as query string
* @return HTTP response with the search results
*/
pub async fn search_files_get(
State(state): State<AppState>,
Query(params): Query<SearchParams>,
) -> impl IntoResponse {
info!("API: Búsqueda de archivos con parámetros: {:?}", params);
info!("API: File search with parameters: {:?}", params);
// Extraer el servicio de búsqueda o devolver error si no está disponible
// Extract the search service or return error if not available
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
error!("Search service not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -47,7 +47,7 @@ impl SearchHandler {
}
};
// Convertir parámetros de búsqueda a DTO
// Convert search parameters to DTO
let search_criteria = SearchCriteriaDto {
name_contains: params.query,
file_types: params.type_filter.map(|t| t.split(',').map(|s| s.trim().to_string()).collect()),
@@ -63,15 +63,15 @@ impl SearchHandler {
offset: params.offset.unwrap_or(0),
};
// Realizar la búsqueda
// Perform the search
match search_service.search(search_criteria).await {
Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
info!("Search completed, {} files and {} folders found",
results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response()
},
Err(err) => {
error!("Error en búsqueda: {}", err);
error!("Search error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
@@ -83,26 +83,26 @@ impl SearchHandler {
}
/**
* Realiza una búsqueda avanzada basada en un objeto de criterios JSON completo.
* Performs an advanced search based on a complete JSON criteria object.
*
* Este endpoint permite búsquedas más complejas con todos los criterios posibles
* proporcionados en el cuerpo de la solicitud.
* This endpoint allows more complex searches with all possible criteria
* provided in the request body.
*
* @param state Estado de la aplicación con servicios
* @param criteria Criterios de búsqueda completos
* @return Respuesta HTTP con los resultados de la búsqueda
* @param state Application state with services
* @param criteria Complete search criteria
* @return HTTP response with the search results
*/
pub async fn search_files_post(
State(state): State<AppState>,
Json(criteria): Json<SearchCriteriaDto>,
) -> impl IntoResponse {
info!("API: Búsqueda avanzada de archivos");
info!("API: Advanced file search");
// Extraer el servicio de búsqueda o devolver error si no está disponible
// Extract the search service or return error if not available
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
error!("Search service not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -112,15 +112,15 @@ impl SearchHandler {
}
};
// Realizar la búsqueda
// Perform the search
match search_service.search(criteria).await {
Ok(results) => {
info!("Búsqueda completada, {} archivos y {} carpetas encontrados",
info!("Search completed, {} files and {} folders found",
results.files.len(), results.folders.len());
(StatusCode::OK, Json(results)).into_response()
},
Err(err) => {
error!("Error en búsqueda: {}", err);
error!("Search error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
@@ -132,24 +132,24 @@ impl SearchHandler {
}
/**
* Limpia la caché de resultados de búsqueda.
* Clears the search results cache.
*
* Este endpoint es útil para forzar búsquedas frescas después de cambios
* significativos en el sistema de archivos.
* This endpoint is useful for forcing fresh searches after significant
* changes in the file system.
*
* @param state Estado de la aplicación con servicios
* @return Respuesta HTTP indicando éxito o error
* @param state Application state with services
* @return HTTP response indicating success or error
*/
pub async fn clear_search_cache(
State(state): State<AppState>,
) -> impl IntoResponse {
info!("API: Limpiando caché de búsqueda");
info!("API: Clearing search cache");
// Extraer el servicio de búsqueda o devolver error si no está disponible
// Extract the search service or return error if not available
let search_service = match &state.applications.search_service {
Some(service) => service,
None => {
error!("Servicio de búsqueda no disponible");
error!("Search service not available");
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -159,10 +159,10 @@ impl SearchHandler {
}
};
// Limpiar la caché
// Clear the cache
match search_service.clear_search_cache().await {
Ok(_) => {
info!("Caché de búsqueda limpiada correctamente");
info!("Search cache cleared successfully");
(
StatusCode::OK,
Json(json!({
@@ -171,7 +171,7 @@ impl SearchHandler {
).into_response()
},
Err(err) => {
error!("Error al limpiar caché de búsqueda: {}", err);
error!("Error clearing search cache: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
@@ -183,43 +183,43 @@ impl SearchHandler {
}
}
/// Parámetros de búsqueda para el endpoint GET
/// Search parameters for the GET endpoint
#[derive(Debug, serde::Deserialize)]
pub struct SearchParams {
/// Texto a buscar en nombres de archivos y carpetas
/// Text to search for in file and folder names
pub query: Option<String>,
/// Filtro por tipos de archivo (extensiones separadas por comas)
/// Filter by file types (comma-separated extensions)
#[serde(rename = "type")]
pub type_filter: Option<String>,
/// Filtrar elementos creados después de esta fecha (timestamp)
/// Filter items created after this date (timestamp)
pub created_after: Option<u64>,
/// Filtrar elementos creados antes de esta fecha (timestamp)
/// Filter items created before this date (timestamp)
pub created_before: Option<u64>,
/// Filtrar elementos modificados después de esta fecha (timestamp)
/// Filter items modified after this date (timestamp)
pub modified_after: Option<u64>,
/// Filtrar elementos modificados antes de esta fecha (timestamp)
/// Filter items modified before this date (timestamp)
pub modified_before: Option<u64>,
/// Tamaño mínimo en bytes
/// Minimum size in bytes
pub min_size: Option<u64>,
/// Tamaño máximo en bytes
/// Maximum size in bytes
pub max_size: Option<u64>,
/// ID de carpeta para limitar la búsqueda
/// Folder ID to limit the search scope
pub folder_id: Option<String>,
/// Búsqueda recursiva en subcarpetas
/// Recursive search in subfolders
pub recursive: Option<bool>,
/// Límite de resultados para paginación
/// Result limit for pagination
pub limit: Option<usize>,
/// Desplazamiento para paginación
/// Offset for pagination
pub offset: Option<usize>,
}
+30 -30
View File
@@ -8,7 +8,7 @@ use tracing::{debug, error, warn, instrument};
use crate::common::di::AppState;
use crate::interfaces::middleware::auth::AuthUser;
/// Obtiene todos los elementos en la papelera para el usuario actual
/// Gets all items in the trash for the current user
#[instrument(skip_all)]
pub async fn get_trash_items(
State(state): State<AppState>,
@@ -19,7 +19,7 @@ pub async fn get_trash_items(
// privilege escalation attacks.
let effective_user = auth_user.id.clone();
debug!("Solicitud para listar elementos en papelera para usuario {}", effective_user);
debug!("Request to list trash items for user {}", effective_user);
let trash_service = match state.trash_service.as_ref() {
Some(service) => service,
@@ -34,11 +34,11 @@ pub async fn get_trash_items(
match result {
Ok(items) => {
debug!("Encontrados {} elementos en la papelera", items.len());
debug!("Found {} items in trash", items.len());
(StatusCode::OK, Json(json!(items)))
},
Err(e) => {
error!("Error al obtener elementos de la papelera: {:?}", e);
error!("Error retrieving trash items: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error retrieving trash items: {}", e)
})))
@@ -46,14 +46,14 @@ pub async fn get_trash_items(
}
}
/// Mueve un elemento (archivo o carpeta) a la papelera (función genérica, no usada directamente en rutas)
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
#[instrument(skip_all)]
pub async fn move_to_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path((item_type, item_id)): Path<(String, String)>,
) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para mover a papelera: tipo={}, id={}, usuario={}",
debug!("Request to move to trash: type={}, id={}, user={}",
item_type, item_id, auth_user.id);
let trash_service = match state.trash_service.as_ref() {
@@ -68,14 +68,14 @@ pub async fn move_to_trash(
match result {
Ok(_) => {
debug!("Elemento movido a papelera con éxito");
debug!("Item moved to trash successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Item moved to trash successfully"
})))
},
Err(e) => {
error!("Error al mover elemento a papelera: {:?}", e);
error!("Error moving item to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving item to trash: {}", e)
})))
@@ -83,14 +83,14 @@ pub async fn move_to_trash(
}
}
/// Mueve un archivo a la papelera
/// Moves a file to the trash
#[instrument(skip_all)]
pub async fn move_file_to_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path(item_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para mover archivo a papelera: id={}, usuario={}",
debug!("Request to move file to trash: id={}, user={}",
item_id, auth_user.id);
let trash_service = match state.trash_service.as_ref() {
@@ -102,19 +102,19 @@ pub async fn move_file_to_trash(
}
};
// Especificar que es un archivo
// Specify that it is a file
let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
match result {
Ok(_) => {
debug!("Archivo movido a papelera con éxito");
debug!("File moved to trash successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "File moved to trash successfully"
})))
},
Err(e) => {
error!("Error al mover archivo a papelera: {:?}", e);
error!("Error moving file to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving file to trash: {}", e)
})))
@@ -122,14 +122,14 @@ pub async fn move_file_to_trash(
}
}
/// Mueve una carpeta a la papelera
/// Moves a folder to the trash
#[instrument(skip_all)]
pub async fn move_folder_to_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path(item_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para mover carpeta a papelera: id={}, usuario={}",
debug!("Request to move folder to trash: id={}, user={}",
item_id, auth_user.id);
let trash_service = match state.trash_service.as_ref() {
@@ -141,19 +141,19 @@ pub async fn move_folder_to_trash(
}
};
// Especificar que es una carpeta
// Specify that it is a folder
let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
match result {
Ok(_) => {
debug!("Carpeta movida a papelera con éxito");
debug!("Folder moved to trash successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Folder moved to trash successfully"
})))
},
Err(e) => {
error!("Error al mover carpeta a papelera: {:?}", e);
error!("Error moving folder to trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error moving folder to trash: {}", e)
})))
@@ -161,14 +161,14 @@ pub async fn move_folder_to_trash(
}
}
/// Restaura un elemento desde la papelera a su ubicación original
/// Restores an item from the trash to its original location
#[instrument(skip_all)]
pub async fn restore_from_trash(
State(state): State<AppState>,
auth_user: AuthUser,
Path(trash_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para restaurar elemento {} de papelera", trash_id);
debug!("Request to restore item {} from trash", trash_id);
let trash_service = match state.trash_service.as_ref() {
Some(service) => service,
@@ -182,7 +182,7 @@ pub async fn restore_from_trash(
match result {
Ok(_) => {
debug!("Elemento restaurado con éxito");
debug!("Item restored successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Item restored successfully"
@@ -199,7 +199,7 @@ pub async fn restore_from_trash(
})));
}
error!("Error al restaurar elemento de papelera: {:?}", e);
error!("Error restoring item from trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error restoring item from trash: {}", e)
})))
@@ -207,14 +207,14 @@ pub async fn restore_from_trash(
}
}
/// Elimina permanentemente un elemento de la papelera
/// Permanently deletes an item from the trash
#[instrument(skip_all)]
pub async fn delete_permanently(
State(state): State<AppState>,
auth_user: AuthUser,
Path(trash_id): Path<String>,
) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para eliminar permanentemente elemento {}", trash_id);
debug!("Request to permanently delete item {}", trash_id);
let trash_service = match state.trash_service.as_ref() {
Some(service) => service,
@@ -228,7 +228,7 @@ pub async fn delete_permanently(
match result {
Ok(_) => {
debug!("Elemento eliminado permanentemente");
debug!("Item permanently deleted");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Item deleted permanently"
@@ -245,7 +245,7 @@ pub async fn delete_permanently(
})));
}
error!("Error al eliminar permanentemente elemento: {:?}", e);
error!("Error permanently deleting item: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error deleting item permanently: {}", e)
})))
@@ -253,13 +253,13 @@ pub async fn delete_permanently(
}
}
/// Vacía la papelera completamente para el usuario actual
/// Empties the trash completely for the current user
#[instrument(skip_all)]
pub async fn empty_trash(
State(state): State<AppState>,
auth_user: AuthUser,
) -> (StatusCode, Json<serde_json::Value>) {
debug!("Solicitud para vaciar papelera del usuario {}", auth_user.id);
debug!("Request to empty trash for user {}", auth_user.id);
let trash_service = match state.trash_service.as_ref() {
Some(service) => service,
@@ -273,14 +273,14 @@ pub async fn empty_trash(
match result {
Ok(_) => {
debug!("Papelera vaciada con éxito");
debug!("Trash emptied successfully");
(StatusCode::OK, Json(json!({
"success": true,
"message": "Trash emptied successfully"
})))
},
Err(e) => {
error!("Error al vaciar papelera: {:?}", e);
error!("Error emptying trash: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({
"error": format!("Error emptying trash: {}", e)
})))
+6 -6
View File
@@ -90,14 +90,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
let favorites_service = app_state.favorites_service.clone();
let recent_service = app_state.recent_service.clone();
// Inicializar el servicio de operaciones por lotes
// Initialize the batch operations service
let batch_service = Arc::new(BatchOperationService::default(
file_retrieval_service.clone(),
file_management_service.clone(),
folder_service.clone()
));
// Crear estado para el manejador de operaciones por lotes
// Create state for the batch operations handler
let batch_handler_state = BatchHandlerState {
batch_service: batch_service.clone(),
};
@@ -154,14 +154,14 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
// Merge the routers
let files_router = basic_file_router.merge(file_operations_router);
// Crear rutas para operaciones por lotes
// Create routes for batch operations
let batch_router = Router::new()
// Operaciones de archivos
// File operations
.route("/files/move", post(batch_handler::move_files_batch))
.route("/files/copy", post(batch_handler::copy_files_batch))
.route("/files/delete", post(batch_handler::delete_files_batch))
.route("/files/get", post(batch_handler::get_files_batch))
// Operaciones de carpetas
// Folder operations
.route("/folders/delete", post(batch_handler::delete_folders_batch))
.route("/folders/create", post(batch_handler::create_folders_batch))
.route("/folders/get", post(batch_handler::get_folders_batch))
@@ -183,7 +183,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
Router::new()
};
// Implementaciones directas de handlers para compartir, sin depender de ShareHandler
// Direct handler implementations for sharing, without depending on ShareHandler
// Create routes for shared resources management (requires auth)
let share_router = if let Some(share_service) = share_service.clone() {
+31 -31
View File
@@ -11,24 +11,24 @@ use crate::common::di::AppState;
// Re-export CurrentUser from application layer for use in handlers
pub use crate::application::dtos::user_dto::CurrentUser;
// Estructura para usar en extractores de Axum
// Structure for use in Axum extractors
#[derive(Clone, Debug)]
pub struct AuthUser {
pub id: String,
pub username: String,
}
/// Extractor reutilizable que obtiene el user_id del usuario autenticado.
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware.
/// Reusable extractor that gets the user_id of the authenticated user.
/// Automatically extracted from the `CurrentUser` inserted by the auth middleware.
///
/// Uso en handlers:
/// Usage in handlers:
/// ```ignore
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
/// ```
#[derive(Clone, Debug)]
pub struct CurrentUserId(pub String);
// Implementar FromRequestParts para AuthUser — permite usar `auth_user: AuthUser` en handlers
// Implement FromRequestParts for AuthUser — allows using `auth_user: AuthUser` in handlers
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
@@ -47,7 +47,7 @@ where
}
}
// Implementar FromRequestParts para CurrentUserId — extractor ligero solo para el user_id
// Implement FromRequestParts for CurrentUserId — lightweight extractor for user_id only
impl<S> FromRequestParts<S> for CurrentUserId
where
S: Send + Sync,
@@ -63,37 +63,37 @@ where
}
}
// Error para las operaciones de autenticación
// Error for authentication operations
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Token no proporcionado")]
#[error("Token not provided")]
TokenNotProvided,
#[error("Token inválido: {0}")]
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Token expirado")]
#[error("Token expired")]
TokenExpired,
#[error("Usuario no encontrado")]
#[error("User not found")]
UserNotFound,
#[error("Acceso denegado: {0}")]
#[error("Access denied: {0}")]
AccessDenied(String),
#[error("Servicio de autenticación no disponible")]
#[error("Authentication service unavailable")]
AuthServiceUnavailable,
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token no proporcionado".to_string()),
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token not provided".to_string()),
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()),
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired".to_string()),
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "User not found".to_string()),
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()),
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Authentication service unavailable".to_string()),
};
let body = axum::Json(serde_json::json!({
@@ -104,24 +104,24 @@ impl IntoResponse for AuthError {
}
}
/// Middleware de autenticación seguro.
/// Secure authentication middleware.
///
/// 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.
/// Validates the JWT token against the configured authentication service.
/// Does not accept bypasses, mock tokens, or URL parameters to skip validation.
pub async fn auth_middleware(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AuthError> {
// Extraer el token Bearer del header Authorization
// Extract the Bearer token from the Authorization header
let token_str = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(AuthError::TokenNotProvided)?;
// Validar que el token no esté vacío
// Validate that the token is not empty
let token_str = token_str.trim();
if token_str.is_empty() {
return Err(AuthError::TokenNotProvided);
@@ -129,7 +129,7 @@ pub async fn auth_middleware(
tracing::debug!("Processing authentication token");
// Validar el token usando el servicio de autenticación
// Validate the token using the authentication service
if let Some(auth_service) = state.auth_service.as_ref() {
let token_service = &auth_service.token_service;
match token_service.validate_token(token_str) {
@@ -146,25 +146,25 @@ pub async fn auth_middleware(
},
Err(e) => {
tracing::warn!("Token validation failed: {}", e);
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
return Err(AuthError::InvalidToken(format!("Invalid token: {}", e)));
}
}
}
// Si no hay servicio de autenticación disponible, denegar acceso
// If no authentication service is available, deny access
tracing::error!("Auth middleware invoked but auth service is not configured");
Err(AuthError::AuthServiceUnavailable)
}
/// Middleware para verificar que el usuario autenticado tiene rol de administrador.
/// Middleware to verify that the authenticated user has an admin role.
///
/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que
/// `CurrentUser` esté presente en las extensiones de la request.
/// Must be applied AFTER auth_middleware, as it depends on
/// `CurrentUser` being present in the request extensions.
pub async fn require_admin(
request: Request,
next: Next,
) -> Response {
// Obtener el CurrentUser insertado por auth_middleware
// Get the CurrentUser inserted by 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);
@@ -175,7 +175,7 @@ pub async fn require_admin(
tracing::warn!("Admin check failed: no authenticated user in request");
}
// Acceso denegado
let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string());
// Access denied
let error = AuthError::AccessDenied("Admin role required".to_string());
error.into_response()
}
+82 -82
View File
@@ -17,39 +17,39 @@ use std::future::Future;
use bytes::Bytes;
use tracing::{debug, info};
const MAX_CACHE_ENTRIES: usize = 1000; // Máximo número de entradas en caché
const DEFAULT_MAX_AGE: u64 = 60; // Tiempo de vida por defecto en segundos
const MAX_CACHE_ENTRIES: usize = 1000; // Maximum number of cache entries
const DEFAULT_MAX_AGE: u64 = 60; // Default time-to-live in seconds
// Definición de tipos para mayor claridad
// Type definitions for clarity
type CacheKey = String;
type EntityTag = String;
/// Un valor almacenado en caché
/// A cached value
#[derive(Clone)]
struct CacheEntry {
/// El ETag calculado para este valor
/// The ETag calculated for this value
etag: EntityTag,
/// Los datos serializados en bytes
/// The serialized data in bytes
data: Option<Bytes>,
/// Las cabeceras originales
/// The original headers
headers: HeaderMap,
/// Timestamp de cuando fue almacenado
/// Timestamp of when it was stored
timestamp: SystemTime,
/// Tiempo de vida en segundos
/// Time-to-live in seconds
max_age: u64,
}
/// Cache para respuestas HTTP con soporte para ETag
/// Cache for HTTP responses with ETag support
#[derive(Clone)]
pub struct HttpCache {
/// Almacenamiento de entradas en caché
/// Cache entry storage
cache: Arc<Mutex<HashMap<CacheKey, CacheEntry>>>,
/// Tiempo de vida por defecto para las entradas
/// Default time-to-live for entries
default_max_age: u64,
}
impl HttpCache {
/// Crea una nueva instancia del caché
/// Creates a new cache instance
pub fn new() -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
@@ -57,7 +57,7 @@ impl HttpCache {
}
}
/// Crea una nueva instancia con un tiempo de vida especificado
/// Creates a new instance with a specified time-to-live
pub fn with_max_age(max_age: u64) -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::with_capacity(100))),
@@ -65,12 +65,12 @@ impl HttpCache {
}
}
/// Obtiene estadísticas del caché
/// Gets cache statistics
pub fn stats(&self) -> (usize, usize) {
let lock = self.cache.lock().unwrap();
let total = lock.len();
// Contar entradas válidas
// Count valid entries
let _now = SystemTime::now();
let valid = lock.values().filter(|entry| {
match entry.timestamp.elapsed() {
@@ -82,12 +82,12 @@ impl HttpCache {
(total, valid)
}
/// Limpia entradas expiradas
/// Cleans up expired entries
pub fn cleanup(&self) -> usize {
let mut lock = self.cache.lock().unwrap();
let initial_count = lock.len();
// Eliminar entradas expiradas
// Remove expired entries
let _now = SystemTime::now();
lock.retain(|_, entry| {
match entry.timestamp.elapsed() {
@@ -102,18 +102,18 @@ impl HttpCache {
removed
}
/// Establece una entrada en el caché
/// Sets an entry in the cache
fn set(&self, key: &str, etag: EntityTag, data: Option<Bytes>, headers: HeaderMap, max_age: Option<u64>) {
let mut lock = self.cache.lock().unwrap();
// Aplicar política de eviction si el caché está lleno
// Apply eviction policy if the cache is full
if lock.len() >= MAX_CACHE_ENTRIES {
debug!("Cache full, removing oldest entries");
// Eliminar el 10% de las entradas más antiguas
// Remove the oldest 10% of entries
self.evict_oldest(&mut lock, MAX_CACHE_ENTRIES / 10);
}
// Almacenar la nueva entrada
// Store the new entry
lock.insert(key.to_string(), CacheEntry {
etag,
data,
@@ -123,30 +123,30 @@ impl HttpCache {
});
}
/// Elimina las entradas más antiguas del caché
/// Removes the oldest entries from the cache
fn evict_oldest(&self, cache: &mut HashMap<CacheKey, CacheEntry>, count: usize) {
// Ordenar por timestamp
// Sort by timestamp
let mut entries: Vec<(CacheKey, SystemTime)> = cache
.iter()
.map(|(key, entry)| (key.clone(), entry.timestamp))
.collect();
// Ordenar por timestamp (más antiguo primero)
// Sort by timestamp (oldest first)
entries.sort_by(|a, b| a.1.cmp(&b.1));
// Eliminar las entradas más antiguas
// Remove the oldest entries
for (key, _) in entries.iter().take(count) {
cache.remove(key);
}
}
/// Obtiene una entrada del caché
/// Gets an entry from the cache
fn get(&self, key: &str) -> Option<CacheEntry> {
let lock = self.cache.lock().unwrap();
// Buscar la entrada
// Look up the entry
if let Some(entry) = lock.get(key) {
// Verificar si ha expirado
// Check if it has expired
match entry.timestamp.elapsed() {
Ok(elapsed) if elapsed.as_secs() < entry.max_age => {
// Entry is still valid
@@ -162,9 +162,9 @@ impl HttpCache {
None
}
/// Genera un ETag simple para un bloque de bytes
/// Generates a simple ETag for a block of bytes
fn calculate_etag_for_bytes(&self, bytes: &[u8]) -> EntityTag {
// Calcular hash
// Calculate hash
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
let hash = hasher.finish();
@@ -173,7 +173,7 @@ impl HttpCache {
}
}
/// Middleware de caché HTTP
/// HTTP cache middleware
pub async fn cache_middleware<T>(
cache: HttpCache,
cache_key: &str,
@@ -184,65 +184,65 @@ pub async fn cache_middleware<T>(
where
T: Serialize
{
// Solo aplicar caché para solicitudes GET
// Only apply cache for GET requests
if req.method() != Method::GET {
return Ok(next.run(req).await);
}
// Verificar si la respuesta está en caché
// Check if the response is cached
let if_none_match = req.headers()
.get("if-none-match")
.and_then(|v| v.to_str().ok());
// Si hay una entrada en caché
// If there is a cache entry
if let Some(cache_entry) = cache.get(cache_key) {
// Comprobar si el cliente ya tiene la versión actualizada
// Check if the client already has the updated version
if let Some(client_etag) = if_none_match {
if client_etag == cache_entry.etag {
// El cliente tiene la versión más reciente, enviar 304 Not Modified
// The client has the most recent version, send 304 Not Modified
debug!("Cache hit (304) for key: {}", cache_key);
return Ok(create_not_modified_response(&cache_entry));
}
}
// El cliente necesita la versión actualizada
// The client needs the updated version
if let Some(data) = &cache_entry.data {
debug!("Cache hit (200) for key: {}", cache_key);
// Crear respuesta con los datos en caché
// Create response with cached data
let mut response = Response::new(Body::from(data.clone()));
// Copiar cabeceras originales
// Copy original headers
for (key, value) in &cache_entry.headers {
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
response.headers_mut().insert(key.clone(), value.clone());
}
}
// Añadir cabeceras de caché
// Add cache headers
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
return Ok(response);
}
}
// No está en caché o ha expirado, continuar con el middleware
// Not cached or expired, continue with the middleware
debug!("Cache miss for key: {}", cache_key);
let response = next.run(req).await;
// No cachear errores
// Don't cache errors
if !response.status().is_success() {
return Ok(response);
}
// Convertir la respuesta para calcular el ETag
// Convert the response to calculate the ETag
let (parts, _body) = response.into_parts();
let bytes = axum::body::to_bytes(_body, 1024 * 1024 * 10).await.unwrap_or_default();
// Calcular ETag
// Calculate ETag
let etag = cache.calculate_etag_for_bytes(&bytes);
// Guardar en caché
// Save to cache
cache.set(
cache_key,
etag.clone(),
@@ -251,26 +251,26 @@ where
max_age
);
// Crear la respuesta con ETag
// Create the response with ETag
let mut response = Response::from_parts(parts, Body::from(bytes));
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache.default_max_age));
Ok(response)
}
/// Crea una respuesta 304 Not Modified
/// Creates a 304 Not Modified response
fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
let mut response = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.body(Body::empty())
.unwrap();
// Copiar cabeceras de caché
// Copy cache headers
if let Some(cache_control) = entry.headers.get("cache-control") {
response.headers_mut().insert("cache-control", cache_control.clone());
}
// Añadir ETag
// Add ETag
response.headers_mut().insert(
"etag",
HeaderValue::from_str(&entry.etag).unwrap_or(HeaderValue::from_static(""))
@@ -279,22 +279,22 @@ fn create_not_modified_response(entry: &CacheEntry) -> Response<Body> {
response
}
/// Configura las cabeceras de caché para una respuesta
/// Configures cache headers for a response
fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
// Añadir ETag
// Add ETag
response.headers_mut().insert(
"etag",
HeaderValue::from_str(etag).unwrap_or(HeaderValue::from_static(""))
);
// Configurar Cache-Control
// Configure Cache-Control
let cache_control = format!("public, max-age={}", max_age);
response.headers_mut().insert(
"cache-control",
HeaderValue::from_str(&cache_control).unwrap_or(HeaderValue::from_static(""))
);
// Añadir cabecera Last-Modified
// Add Last-Modified header
let now: DateTime<Utc> = Utc::now();
let last_modified = now.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
response.headers_mut().insert(
@@ -303,7 +303,7 @@ fn set_cache_headers(response: &mut Response<Body>, etag: &str, max_age: u64) {
);
}
/// Layer para aplicar middleware de caché
/// Layer for applying cache middleware
#[derive(Clone)]
pub struct HttpCacheLayer {
cache: HttpCache,
@@ -311,7 +311,7 @@ pub struct HttpCacheLayer {
}
impl HttpCacheLayer {
/// Crea una nueva capa de caché
/// Creates a new cache layer
pub fn new(cache: HttpCache) -> Self {
Self {
cache,
@@ -319,7 +319,7 @@ impl HttpCacheLayer {
}
}
/// Establece el tiempo de vida máximo
/// Sets the maximum time-to-live
pub fn with_max_age(mut self, max_age: u64) -> Self {
self.max_age = Some(max_age);
self
@@ -338,7 +338,7 @@ impl<S> Layer<S> for HttpCacheLayer {
}
}
/// Servicio que implementa la lógica de caché
/// Service that implements cache logic
#[derive(Clone)]
pub struct HttpCacheService<S> {
inner: S,
@@ -365,10 +365,10 @@ where
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
// Generar clave de caché
// Generate cache key
let cache_key = req.uri().path().to_string();
// Solo aplicar caché para solicitudes GET
// Only apply cache for GET requests
if req.method() != Method::GET {
let future = self.inner.call(req);
return Box::pin(async move {
@@ -377,42 +377,42 @@ where
});
}
// Obtener ETag del cliente
// Get client ETag
let if_none_match = req.headers()
.get("if-none-match")
.and_then(|v| v.to_str().ok());
// Verificar si hay una entrada en caché
// Check if there is a cache entry
let cache_clone = self.cache.clone();
let max_age = self.max_age;
let entry = cache_clone.get(&cache_key);
match entry {
Some(cache_entry) if if_none_match == Some(&cache_entry.etag) => {
// El cliente tiene la versión correcta, enviar 304
// The client has the correct version, send 304
debug!("Cache HIT (304): {}", cache_key);
let response = create_not_modified_response(&cache_entry);
return Box::pin(async move { Ok(response) });
},
Some(cache_entry) if cache_entry.data.is_some() => {
// El cliente necesita la versión actualizada
// The client needs the updated version
debug!("Cache HIT (200): {}", cache_key);
let mut response = Response::new(Body::from(cache_entry.data.clone().unwrap()));
// Copiar cabeceras originales
// Copy original headers
for (key, value) in &cache_entry.headers {
if !key.as_str().eq_ignore_ascii_case("transfer-encoding") {
response.headers_mut().insert(key.clone(), value.clone());
}
}
// Añadir cabeceras de caché
// Add cache headers
set_cache_headers(&mut response, &cache_entry.etag, max_age.unwrap_or(cache_entry.max_age));
return Box::pin(async move { Ok(response) });
},
_ => {
// No está en caché o ha expirado
// Not cached or expired
debug!("Cache MISS: {}", cache_key);
let future = self.inner.call(req);
let cache_clone = self.cache.clone();
@@ -423,19 +423,19 @@ where
let response = future.await.map_err(|e| e.into())?;
let response = response_map_body(response).await;
// No cachear errores
// Don't cache errors
if !response.status().is_success() {
return Ok(response);
}
// Obtener el cuerpo y calcular ETag
// Get the body and calculate ETag
let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024 * 10).await?;
// Calcular ETag
// Calculate ETag
let etag = cache_clone.calculate_etag_for_bytes(&bytes);
// Guardar en caché
// Save to cache
cache_clone.set(
&cache_key,
etag.clone(),
@@ -444,7 +444,7 @@ where
max_age
);
// Crear la respuesta con ETag
// Create the response with ETag
let mut response = Response::from_parts(parts, Body::from(bytes));
set_cache_headers(&mut response, &etag, max_age.unwrap_or(cache_clone.default_max_age));
@@ -455,9 +455,9 @@ where
}
}
// 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.
// Helper function to convert any body into Body preserving its content.
// Previously this function discarded the body with Body::empty(), causing
// data loss in non-cached responses.
async fn response_map_body<B>(response: Response<B>) -> Response<Body>
where
B: http_body::Body + Send + 'static,
@@ -478,10 +478,10 @@ where
Response::from_parts(parts, Body::from(collected))
}
/// Inicia una tarea de limpieza periódica para el caché
/// Starts a periodic cleanup task for the cache
pub fn start_cache_cleanup_task(cache: HttpCache) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Cada 5 minutos
let mut interval = tokio::time::interval(Duration::from_secs(300)); // Every 5 minutes
loop {
interval.tick().await;
@@ -516,10 +516,10 @@ mod tests {
let etag2 = cache.calculate_etag_for_bytes(&data2);
let etag3 = cache.calculate_etag_for_bytes(&data3);
// Mismos datos deben generar mismo ETag
// Same data should generate the same ETag
assert_eq!(etag1, etag2);
// Datos diferentes deben generar ETags diferentes
// Different data should generate different ETags
assert_ne!(etag1, etag3);
}
@@ -527,19 +527,19 @@ mod tests {
async fn test_cache_hit_miss() {
let cache = HttpCache::new();
// Crear datos de prueba directamente como Bytes
// Create test data directly as 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()), headers1, None);
// Verificar cache hit
// Verify cache hit
let entry = cache.get("test").unwrap();
assert_eq!(entry.etag, etag1);
assert_eq!(entry.data.unwrap(), bytes1);
// Verificar cache miss
// Verify cache miss
assert!(cache.get("nonexistent").is_none());
}
}