refactor: remove serde from domain entities for Clean Architecture compliance
- Remove Serialize/Deserialize from File, Folder, Session, User, Contact entities - Create contact_persistence_dto.rs for JSONB persistence in infrastructure layer - Update contact_pg_repository to use persistence DTOs - Fix dependency on zip crate (downgrade from 7.2.0 to 2.1.0) - Fix unused variable warnings in main.rs - Move PathService import from domain to infrastructure - Add missing fields to CoreServices and RepositoryServices - Create proper service initialization in main.rs Clean Architecture improvements: - Domain layer no longer depends on serde framework - Persistence concerns isolated to infrastructure layer - TokenClaims in auth_service.rs is only exception (required for JWT)
This commit is contained in:
@@ -9,11 +9,9 @@ use tracing::debug;
|
||||
pub const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; // 64KB
|
||||
|
||||
/// Número máximo por defecto de buffers en el pool
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_MAX_BUFFERS: usize = 100;
|
||||
|
||||
/// Tiempo de vida por defecto de un buffer inactivo (en segundos)
|
||||
#[allow(dead_code)]
|
||||
pub const DEFAULT_BUFFER_TTL: u64 = 60;
|
||||
|
||||
/// Buffer pooling para optimizar operaciones de lectura/escritura
|
||||
@@ -83,7 +81,6 @@ impl BufferPool {
|
||||
}
|
||||
|
||||
/// Crea un pool con configuración por defecto
|
||||
#[allow(dead_code)]
|
||||
pub fn default() -> Arc<Self> {
|
||||
Self::new(
|
||||
DEFAULT_BUFFER_SIZE,
|
||||
@@ -282,7 +279,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Obtiene una referencia a los datos utilizados
|
||||
#[allow(dead_code)]
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
&self.buffer[..self.used_size]
|
||||
}
|
||||
@@ -302,7 +298,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Copia datos a este buffer y actualiza el tamaño usado
|
||||
#[allow(dead_code)]
|
||||
pub fn copy_from_slice(&mut self, data: &[u8]) -> usize {
|
||||
let copy_size = min(data.len(), self.buffer.len());
|
||||
self.buffer[..copy_size].copy_from_slice(&data[..copy_size]);
|
||||
@@ -311,7 +306,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Impide que el buffer se devuelva al pool al destruirse
|
||||
#[allow(dead_code)]
|
||||
pub fn do_not_return(mut self) -> Self {
|
||||
self.return_to_pool = false;
|
||||
self
|
||||
@@ -323,7 +317,6 @@ impl BorrowedBuffer {
|
||||
}
|
||||
|
||||
/// Obtiene el tamaño usado del buffer
|
||||
#[allow(dead_code)]
|
||||
pub fn used_size(&self) -> usize {
|
||||
self.used_size
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use tokio::sync::RwLock;
|
||||
|
||||
/// Representación de metadatos en caché
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CachedMetadata {
|
||||
/// Si el archivo o directorio existe
|
||||
pub exists: bool,
|
||||
@@ -23,7 +22,6 @@ pub struct CachedMetadata {
|
||||
}
|
||||
|
||||
/// Estructura para gestionar la caché de metadatos de archivos y directorios
|
||||
#[allow(dead_code)]
|
||||
pub struct StorageCacheManager {
|
||||
/// Caché de existencia y metadatos
|
||||
cache: RwLock<HashMap<PathBuf, CachedMetadata>>,
|
||||
@@ -37,7 +35,6 @@ pub struct StorageCacheManager {
|
||||
|
||||
impl StorageCacheManager {
|
||||
/// Crea una nueva instancia del gestor de caché
|
||||
#[allow(dead_code)]
|
||||
pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self {
|
||||
Self {
|
||||
cache: RwLock::new(HashMap::with_capacity(max_entries)),
|
||||
@@ -48,7 +45,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Crea una instancia por defecto del gestor de caché
|
||||
#[allow(dead_code)]
|
||||
pub fn default() -> Self {
|
||||
Self::new(
|
||||
60_000, // 1 minuto para archivos
|
||||
@@ -58,7 +54,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Verifica si un archivo o directorio existe en caché
|
||||
#[allow(dead_code)]
|
||||
pub async fn check_exists(&self, path: &PathBuf, _is_dir: bool) -> Result<bool, ()> {
|
||||
// Intentar obtener de la caché
|
||||
if let Some(metadata) = self.get_cached_metadata(path).await {
|
||||
@@ -70,7 +65,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Obtiene los metadatos de un path desde la caché
|
||||
#[allow(dead_code)]
|
||||
async fn get_cached_metadata(&self, path: &PathBuf) -> Option<CachedMetadata> {
|
||||
let cache = self.cache.read().await;
|
||||
|
||||
@@ -85,7 +79,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Actualiza la caché con los metadatos de un path
|
||||
#[allow(dead_code)]
|
||||
pub async fn update_cache(&self, path: &PathBuf, exists: bool, size: Option<u64>,
|
||||
created_at: Option<u64>, modified_at: Option<u64>, is_dir: bool) {
|
||||
let mut cache = self.cache.write().await;
|
||||
@@ -115,7 +108,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Elimina entradas aleatorias de la caché cuando está llena
|
||||
#[allow(dead_code)]
|
||||
async fn evict_entries(&self, cache: &mut HashMap<PathBuf, CachedMetadata>, count: usize) {
|
||||
// Obtener las entradas más antiguas para eliminar
|
||||
let mut entries: Vec<_> = cache.keys().cloned().collect();
|
||||
@@ -136,7 +128,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Inicia una tarea de limpieza periódica
|
||||
#[allow(dead_code)]
|
||||
pub fn start_cleanup_task(cache_manager: Arc<Self>) -> BoxFuture<'static, ()> {
|
||||
Box::pin(async move {
|
||||
let interval = Duration::from_secs(60); // Ejecutar cada minuto
|
||||
@@ -170,14 +161,12 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Invalida una entrada específica de la caché
|
||||
#[allow(dead_code)]
|
||||
pub async fn invalidate(&self, path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.remove(path);
|
||||
}
|
||||
|
||||
/// Invalida todas las entradas de la caché relacionadas con una carpeta
|
||||
#[allow(dead_code)]
|
||||
pub async fn invalidate_folder(&self, folder_path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
@@ -204,7 +193,6 @@ impl StorageCacheManager {
|
||||
}
|
||||
|
||||
/// Obtiene el número actual de entradas en la caché
|
||||
#[allow(dead_code)]
|
||||
pub async fn cache_size(&self) -> usize {
|
||||
let cache = self.cache.read().await;
|
||||
cache.len()
|
||||
|
||||
@@ -48,14 +48,12 @@ pub trait CompressionService: Send + Sync {
|
||||
async fn decompress_data(&self, compressed_data: &[u8]) -> io::Result<Vec<u8>>;
|
||||
|
||||
/// Comprime un stream de datos
|
||||
#[allow(dead_code)]
|
||||
fn compress_stream<S>(&self, stream: S, level: CompressionLevel)
|
||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||
where
|
||||
S: Stream<Item = io::Result<Bytes>> + Send + 'static + Unpin;
|
||||
|
||||
/// Descomprime un stream de datos
|
||||
#[allow(dead_code)]
|
||||
fn decompress_stream<S>(&self, compressed_stream: S)
|
||||
-> impl Stream<Item = io::Result<Bytes>> + Send
|
||||
where
|
||||
|
||||
@@ -47,14 +47,12 @@ pub struct FileMetadata {
|
||||
/// Ruta absoluta del archivo
|
||||
pub path: PathBuf,
|
||||
/// Si el archivo existe físicamente
|
||||
#[allow(dead_code)]
|
||||
pub exists: bool,
|
||||
/// Tipo de entrada (archivo, directorio)
|
||||
pub entry_type: CacheEntryType,
|
||||
/// Tamaño en bytes (para archivos)
|
||||
pub size: Option<u64>,
|
||||
/// Tipo MIME (para archivos)
|
||||
#[allow(dead_code)]
|
||||
pub mime_type: Option<String>,
|
||||
/// Timestamp de creación (UNIX epoch seconds)
|
||||
pub created_at: Option<u64>,
|
||||
@@ -259,7 +257,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Verifica si un archivo existe
|
||||
#[allow(dead_code)]
|
||||
pub async fn exists(&self, path: &Path) -> Option<bool> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return Some(metadata.exists);
|
||||
@@ -269,7 +266,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Verifica si un path es un directorio
|
||||
#[allow(dead_code)]
|
||||
pub async fn is_dir(&self, path: &Path) -> Option<bool> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return Some(metadata.entry_type == CacheEntryType::Directory);
|
||||
@@ -288,7 +284,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Obtiene el tamaño de un archivo
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_size(&self, path: &Path) -> Option<u64> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return metadata.size;
|
||||
@@ -298,7 +293,6 @@ impl FileMetadataCache {
|
||||
}
|
||||
|
||||
/// Obtiene el tipo MIME de un archivo
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_mime_type(&self, path: &Path) -> Option<String> {
|
||||
if let Some(metadata) = self.get_metadata(path).await {
|
||||
return metadata.mime_type;
|
||||
|
||||
@@ -301,7 +301,6 @@ impl IdMappingOptimizer {
|
||||
}
|
||||
|
||||
/// Precargar un conjunto de rutas para obtener sus IDs en batch
|
||||
#[allow(dead_code)]
|
||||
pub async fn preload_paths(&self, paths: Vec<StoragePath>) -> Result<(), IdMappingError> {
|
||||
// Solo proceder si hay rutas para cargar
|
||||
if paths.is_empty() {
|
||||
@@ -342,7 +341,6 @@ impl IdMappingOptimizer {
|
||||
}
|
||||
|
||||
/// Precargar un conjunto de IDs para obtener sus rutas en batch
|
||||
#[allow(dead_code)]
|
||||
pub async fn preload_ids(&self, ids: Vec<String>) -> Result<(), IdMappingError> {
|
||||
// Solo proceder si hay IDs para cargar
|
||||
if ids.is_empty() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{RwLock, Mutex};
|
||||
use tokio::fs;
|
||||
use tokio::time;
|
||||
@@ -29,7 +28,6 @@ pub enum IdMappingError {
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
#[allow(dead_code)]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
@@ -69,9 +67,6 @@ struct IdMap {
|
||||
version: u32, // Versión para detectar cambios
|
||||
}
|
||||
|
||||
/// Constantes para configuración
|
||||
const SAVE_DEBOUNCE_MS: u64 = 0; // Sin debounce para garantizar guardado inmediato
|
||||
|
||||
/// Servicio para gestionar mapeos entre rutas y IDs únicos
|
||||
pub struct IdMappingService {
|
||||
map_path: PathBuf,
|
||||
@@ -490,7 +485,6 @@ impl IdMappingPort for IdMappingService {
|
||||
/// Synchronous helper for contexts where we can't use async
|
||||
impl IdMappingService {
|
||||
/// Create a new service synchronously (only for stubs and initialization)
|
||||
#[allow(dead_code)]
|
||||
pub fn new_sync(map_path: PathBuf) -> Self {
|
||||
// Create a minimal implementation for initialization purposes
|
||||
Self {
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
//! JWT-based token service implementation.
|
||||
//!
|
||||
//! This module provides JWT token generation and validation functionality,
|
||||
//! implementing the TokenServicePort trait defined in the application layer.
|
||||
|
||||
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use uuid::Uuid;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::application::ports::auth_ports::{TokenServicePort, TokenClaims};
|
||||
use crate::domain::entities::user::User;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Internal JWT claims structure for serialization.
|
||||
/// This is the actual JWT payload structure used by jsonwebtoken crate.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct JwtClaims {
|
||||
/// Subject identifier - contains the user ID
|
||||
pub sub: String,
|
||||
/// Expiration timestamp (seconds since Unix epoch)
|
||||
pub exp: i64,
|
||||
/// Issued at timestamp (seconds since Unix epoch)
|
||||
pub iat: i64,
|
||||
/// JWT unique ID for token tracking and revocation
|
||||
pub jti: String,
|
||||
/// Username for display and identification purposes
|
||||
pub username: String,
|
||||
/// User email for communication and identification
|
||||
pub email: String,
|
||||
/// User role for authorization checks
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
impl From<JwtClaims> for TokenClaims {
|
||||
fn from(claims: JwtClaims) -> Self {
|
||||
TokenClaims {
|
||||
sub: claims.sub,
|
||||
exp: claims.exp,
|
||||
iat: claims.iat,
|
||||
jti: claims.jti,
|
||||
username: claims.username,
|
||||
email: claims.email,
|
||||
role: claims.role,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JWT-based implementation of the TokenServicePort.
|
||||
///
|
||||
/// This service handles JWT token generation and validation for user authentication.
|
||||
/// It uses HS256 algorithm for signing tokens.
|
||||
pub struct JwtTokenService {
|
||||
/// Secret key used for signing JWT tokens
|
||||
jwt_secret: String,
|
||||
/// Expiration time for access tokens in seconds
|
||||
access_token_expiry: i64,
|
||||
/// Expiration time for refresh tokens in seconds
|
||||
refresh_token_expiry: i64,
|
||||
}
|
||||
|
||||
impl JwtTokenService {
|
||||
/// Create a new JwtTokenService with the specified configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `jwt_secret` - Secret key for signing tokens (should be at least 32 bytes)
|
||||
/// * `access_token_expiry_secs` - Lifetime of access tokens in seconds
|
||||
/// * `refresh_token_expiry_secs` - Lifetime of refresh tokens in seconds
|
||||
pub fn new(jwt_secret: String, access_token_expiry_secs: i64, refresh_token_expiry_secs: i64) -> Self {
|
||||
Self {
|
||||
jwt_secret,
|
||||
access_token_expiry: access_token_expiry_secs,
|
||||
refresh_token_expiry: refresh_token_expiry_secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenServicePort for JwtTokenService {
|
||||
fn generate_access_token(&self, user: &User) -> Result<String, DomainError> {
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
// Log information for debugging
|
||||
tracing::debug!(
|
||||
"Generating token for user: {}, id: {}, role: {}",
|
||||
user.username(),
|
||||
user.id(),
|
||||
user.role()
|
||||
);
|
||||
|
||||
let claims = JwtClaims {
|
||||
sub: user.id().to_string(),
|
||||
exp: now + self.access_token_expiry,
|
||||
iat: now,
|
||||
jti: Uuid::new_v4().to_string(),
|
||||
username: user.username().to_string(),
|
||||
email: user.email().to_string(),
|
||||
role: format!("{}", user.role()),
|
||||
};
|
||||
|
||||
// Log JWT claims for debugging
|
||||
tracing::debug!("JWT claims: sub={}, exp={}, iat={}", claims.sub, claims.exp, claims.iat);
|
||||
|
||||
encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(self.jwt_secret.as_bytes())
|
||||
)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Error generating token: {}", e);
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"TokenService",
|
||||
format!("Error al generar token: {}", e)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError> {
|
||||
let validation = Validation::new(Algorithm::HS256);
|
||||
|
||||
let token_data = decode::<JwtClaims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(self.jwt_secret.as_bytes()),
|
||||
&validation
|
||||
)
|
||||
.map_err(|e| {
|
||||
match e.kind() {
|
||||
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
|
||||
DomainError::new(ErrorKind::AccessDenied, "TokenService", "Token expirado")
|
||||
},
|
||||
_ => DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"TokenService",
|
||||
format!("Token inválido: {}", e)
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(token_data.claims.into())
|
||||
}
|
||||
|
||||
fn generate_refresh_token(&self) -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
fn refresh_token_expiry_secs(&self) -> i64 {
|
||||
self.refresh_token_expiry
|
||||
}
|
||||
|
||||
fn refresh_token_expiry_days(&self) -> i64 {
|
||||
self.refresh_token_expiry / (24 * 3600)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
|
||||
fn create_test_user() -> User {
|
||||
User::from_data(
|
||||
"test-user-id".to_string(),
|
||||
"testuser".to_string(),
|
||||
"test@example.com".to_string(),
|
||||
"hashed_password".to_string(),
|
||||
UserRole::User,
|
||||
1024 * 1024 * 1024, // 1GB
|
||||
0,
|
||||
chrono::Utc::now(),
|
||||
chrono::Utc::now(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_and_validate_token() {
|
||||
let service = JwtTokenService::new(
|
||||
"test_secret_key_at_least_32_bytes_long".to_string(),
|
||||
3600, // 1 hour
|
||||
86400, // 1 day
|
||||
);
|
||||
|
||||
let user = create_test_user();
|
||||
let token = service.generate_access_token(&user).expect("Should generate token");
|
||||
|
||||
let claims = service.validate_token(&token).expect("Should validate token");
|
||||
assert_eq!(claims.sub, user.id());
|
||||
assert_eq!(claims.username, user.username());
|
||||
assert_eq!(claims.email, user.email());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refresh_token_is_unique() {
|
||||
let service = JwtTokenService::new("secret".to_string(), 3600, 86400);
|
||||
|
||||
let token1 = service.generate_refresh_token();
|
||||
let token2 = service.generate_refresh_token();
|
||||
|
||||
assert_ne!(token1, token2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_token() {
|
||||
let service = JwtTokenService::new("secret".to_string(), 3600, 86400);
|
||||
|
||||
let result = service.validate_token("invalid_token");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,7 @@ pub mod file_metadata_cache;
|
||||
pub mod compression_service;
|
||||
pub mod buffer_pool;
|
||||
pub mod trash_cleanup_service;
|
||||
pub mod zip_service;
|
||||
pub mod zip_service;
|
||||
pub mod path_service;
|
||||
pub mod password_hasher;
|
||||
pub mod jwt_service;
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Argon2-based password hasher implementation.
|
||||
//!
|
||||
//! This module provides a secure password hashing implementation using the Argon2id
|
||||
//! algorithm, which is the recommended choice for password hashing as of 2023+.
|
||||
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use argon2::password_hash::SaltString;
|
||||
use rand_core::OsRng;
|
||||
|
||||
use crate::application::ports::auth_ports::PasswordHasherPort;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Argon2-based implementation of the PasswordHasherPort.
|
||||
///
|
||||
/// Uses Argon2id algorithm which provides resistance against both side-channel
|
||||
/// and GPU-based attacks. This is the recommended algorithm for password hashing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Argon2PasswordHasher {
|
||||
/// Argon2 hasher instance - uses default secure parameters
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl Argon2PasswordHasher {
|
||||
/// Create a new Argon2PasswordHasher with default secure parameters.
|
||||
pub fn new() -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Argon2PasswordHasher {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PasswordHasherPort for Argon2PasswordHasher {
|
||||
fn hash_password(&self, password: &str) -> Result<String, DomainError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
argon2.hash_password(password.as_bytes(), &salt)
|
||||
.map(|hash| hash.to_string())
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Error al generar hash de password: {}", e)
|
||||
))
|
||||
}
|
||||
|
||||
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError> {
|
||||
let parsed_hash = PasswordHash::new(hash)
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"PasswordHasher",
|
||||
format!("Error al procesar hash: {}", e)
|
||||
))?;
|
||||
|
||||
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hash_and_verify_password() {
|
||||
let hasher = Argon2PasswordHasher::new();
|
||||
let password = "test_password_123";
|
||||
|
||||
let hash = hasher.hash_password(password).expect("Should hash password");
|
||||
assert!(hasher.verify_password(password, &hash).expect("Should verify"));
|
||||
assert!(!hasher.verify_password("wrong_password", &hash).expect("Should verify"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_hashes_for_same_password() {
|
||||
let hasher = Argon2PasswordHasher::new();
|
||||
let password = "same_password";
|
||||
|
||||
let hash1 = hasher.hash_password(password).expect("Should hash");
|
||||
let hash2 = hasher.hash_password(password).expect("Should hash");
|
||||
|
||||
// Hashes should be different due to random salt
|
||||
assert_ne!(hash1, hash2);
|
||||
|
||||
// But both should verify correctly
|
||||
assert!(hasher.verify_password(password, &hash1).expect("Should verify"));
|
||||
assert!(hasher.verify_password(password, &hash2).expect("Should verify"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
//! PathService - Servicio de infraestructura para manejo de rutas de almacenamiento
|
||||
//!
|
||||
//! Este servicio fue movido desde domain/services porque implementa traits de application
|
||||
//! (StoragePort, StorageMediator) y tiene dependencias de sistema de archivos (tokio::fs).
|
||||
//!
|
||||
//! StoragePath (Value Object) permanece en domain/services/path_service.rs
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use async_trait::async_trait;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::application::ports::outbound::StoragePort;
|
||||
use crate::application::services::storage_mediator::{StorageMediator, StorageMediatorResult, StorageMediatorError};
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
|
||||
/// Servicio de infraestructura para manejar operaciones con rutas de almacenamiento
|
||||
pub struct PathService {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl PathService {
|
||||
/// Crea un nuevo servicio de rutas con una raíz específica
|
||||
pub fn new(root_path: PathBuf) -> Self {
|
||||
Self { root_path }
|
||||
}
|
||||
|
||||
/// Convierte una ruta del dominio a una ruta física absoluta
|
||||
pub fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Convierte una ruta física a una ruta de dominio
|
||||
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
|
||||
physical_path.strip_prefix(&self.root_path).ok().map(|rel_path| {
|
||||
let segments: Vec<String> = rel_path
|
||||
.components()
|
||||
.filter_map(|c| match c {
|
||||
std::path::Component::Normal(os_str) => Some(os_str.to_string_lossy().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
StoragePath::new(segments)
|
||||
})
|
||||
}
|
||||
|
||||
/// Crea una ruta de archivo dentro de una carpeta
|
||||
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
|
||||
folder_path.join(file_name)
|
||||
}
|
||||
|
||||
/// Verifica si una ruta es directamente hija de otra
|
||||
pub fn is_direct_child(&self, parent_path: &StoragePath, potential_child: &StoragePath) -> bool {
|
||||
if let Some(child_parent) = potential_child.parent() {
|
||||
&child_parent == parent_path
|
||||
} else {
|
||||
parent_path.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si una ruta está en la raíz
|
||||
pub fn is_in_root(&self, path: &StoragePath) -> bool {
|
||||
path.parent().map_or(true, |p| p.is_empty())
|
||||
}
|
||||
|
||||
/// Gets the root path used by this service
|
||||
pub fn get_root_path(&self) -> &Path {
|
||||
&self.root_path
|
||||
}
|
||||
|
||||
/// Valida una ruta para asegurar que no contiene componentes peligrosos
|
||||
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Verificar que no haya segmentos vacíos
|
||||
if path.segments().iter().any(|s| s.is_empty()) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains empty segments: {}", path.to_string())
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no haya caracteres peligrosos
|
||||
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
|
||||
for segment in path.segments() {
|
||||
if segment.contains(&dangerous_chars[..]) {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path contains dangerous characters: {}", segment)
|
||||
));
|
||||
}
|
||||
|
||||
// Verificar que no empiece con . (oculto en Unix)
|
||||
if segment.starts_with('.') && segment != ".well-known" {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Path",
|
||||
format!("Path segments cannot start with dot: {}", segment)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StoragePort for PathService {
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
let mut path = self.root_path.clone();
|
||||
for segment in storage_path.segments() {
|
||||
path.push(segment);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
|
||||
// Primero validar la ruta
|
||||
self.validate_path(storage_path)?;
|
||||
|
||||
// Resolver a ruta física
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
// Crear directorios si no existen
|
||||
if !physical_path.exists() {
|
||||
fs::create_dir_all(&physical_path).await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"Storage",
|
||||
format!("Failed to create directory: {}", physical_path.display())
|
||||
).with_source(e))?;
|
||||
|
||||
tracing::debug!("Created directory: {}", physical_path.display());
|
||||
} else if !physical_path.is_dir() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Storage",
|
||||
format!("Path exists but is not a directory: {}", physical_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_file();
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
|
||||
let physical_path = self.resolve_path(storage_path);
|
||||
|
||||
let exists = physical_path.exists() && physical_path.is_dir();
|
||||
Ok(exists)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StorageMediator for PathService {
|
||||
async fn get_folder_path(&self, folder_id: &str) -> StorageMediatorResult<PathBuf> {
|
||||
// This is a simplified implementation since PathService doesn't have direct
|
||||
// access to folder repository. It's typically used through a proxy.
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder_storage_path(&self, folder_id: &str) -> StorageMediatorResult<StoragePath> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn get_folder(&self, folder_id: &str) -> StorageMediatorResult<Folder> {
|
||||
// Simplified implementation - should be overridden by actual implementations
|
||||
Err(StorageMediatorError::NotFound(format!("Folder with ID {} not found", folder_id)))
|
||||
}
|
||||
|
||||
async fn file_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn file_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_file())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_path(&self, path: &Path) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(&StoragePath::from_string(&path.to_string_lossy()));
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
async fn folder_exists_at_storage_path(&self, storage_path: &StoragePath) -> StorageMediatorResult<bool> {
|
||||
let abs_path = self.resolve_path(storage_path);
|
||||
Ok(abs_path.exists() && abs_path.is_dir())
|
||||
}
|
||||
|
||||
fn resolve_path(&self, relative_path: &Path) -> PathBuf {
|
||||
// Convert path to storage path then resolve
|
||||
let path_str = relative_path.to_string_lossy().to_string();
|
||||
let storage_path = StoragePath::from_string(&path_str);
|
||||
PathService::resolve_path(self, &storage_path)
|
||||
}
|
||||
|
||||
fn resolve_storage_path(&self, storage_path: &StoragePath) -> PathBuf {
|
||||
PathService::resolve_path(self, storage_path)
|
||||
}
|
||||
|
||||
async fn ensure_directory(&self, path: &Path) -> StorageMediatorResult<()> {
|
||||
let abs_path = PathService::resolve_path(self, &StoragePath::from_string(&path.to_string_lossy()));
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_storage_directory(&self, storage_path: &StoragePath) -> StorageMediatorResult<()> {
|
||||
let abs_path = PathService::resolve_path(self, storage_path);
|
||||
|
||||
if !abs_path.exists() {
|
||||
fs::create_dir_all(&abs_path).await
|
||||
.map_err(|e| StorageMediatorError::AccessError(format!("Failed to create directory: {}", e)))?;
|
||||
} else if !abs_path.is_dir() {
|
||||
return Err(StorageMediatorError::InvalidPath(
|
||||
format!("Path exists but is not a directory: {}", abs_path.display())
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let storage_path = StoragePath::from_string("test/file.txt");
|
||||
let absolute = service.resolve_path(&storage_path);
|
||||
|
||||
assert_eq!(absolute, PathBuf::from("/storage/test/file.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_storage_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let physical_path = PathBuf::from("/storage/folder/file.txt");
|
||||
let storage_path = service.to_storage_path(&physical_path).unwrap();
|
||||
|
||||
assert_eq!(storage_path.to_string(), "/folder/file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_in_root() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let root_path = StoragePath::from_string("file.txt");
|
||||
let nested_path = StoragePath::from_string("folder/file.txt");
|
||||
|
||||
assert!(service.is_in_root(&root_path));
|
||||
assert!(!service.is_in_root(&nested_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_direct_child() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let parent = StoragePath::from_string("folder");
|
||||
let child = StoragePath::from_string("folder/file.txt");
|
||||
let not_child = StoragePath::from_string("folder2/file.txt");
|
||||
|
||||
assert!(service.is_direct_child(&parent, &child));
|
||||
assert!(!service.is_direct_child(&parent, ¬_child));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_file_path() {
|
||||
let service = PathService::new(PathBuf::from("/storage"));
|
||||
|
||||
let folder_path = StoragePath::from_string("folder");
|
||||
let file_path = service.create_file_path(&folder_path, "file.txt");
|
||||
|
||||
assert_eq!(file_path.to_string(), "/folder/file.txt");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user