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:
Dionisio
2026-02-02 23:56:40 +01:00
parent 6aceb07f3f
commit 52840e57df
88 changed files with 4286 additions and 4847 deletions
+3 -6
View File
@@ -48,7 +48,6 @@ impl CalDavAdapter {
let mut in_sync_collection = false;
let mut in_prop = false;
let mut in_filter = false;
let mut in_time_range = false;
let mut start_time: Option<DateTime<Utc>> = None;
let mut end_time: Option<DateTime<Utc>> = None;
let mut props = Vec::new();
@@ -68,8 +67,6 @@ impl CalDavAdapter {
s if s == "prop" || s.ends_with(":prop") => in_prop = true,
s if s == "filter" || s.ends_with(":filter") => in_filter = true,
s if s == "time-range" || s.ends_with(":time-range") => {
in_time_range = true;
// Parse time-range attributes
for attr in e.attributes() {
if let Ok(attr) = attr {
@@ -106,7 +103,7 @@ impl CalDavAdapter {
}
},
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap_or_default();
let text = e.decode().unwrap_or_default();
// Check if we're in sync-token element
if in_sync_collection && !in_prop && !in_filter {
@@ -128,7 +125,7 @@ impl CalDavAdapter {
s if s == "sync-collection" || s.ends_with(":sync-collection") => in_sync_collection = false,
s if s == "prop" || s.ends_with(":prop") => in_prop = false,
s if s == "filter" || s.ends_with(":filter") => in_filter = false,
s if s == "time-range" || s.ends_with(":time-range") => in_time_range = false,
s if s == "time-range" || s.ends_with(":time-range") => { /* time-range end, attributes already parsed */ },
_ => ()
}
},
@@ -771,7 +768,7 @@ impl CalDavAdapter {
}
},
Ok(Event::Text(e)) => {
let text = e.unescape().unwrap_or_default();
let text = e.decode().unwrap_or_default();
if in_displayname {
displayname = text.to_string();
+2 -2
View File
@@ -685,7 +685,7 @@ impl WebDavAdapter {
},
Ok(Event::Text(e)) => {
if current_prop.is_some() {
current_text.push_str(&e.unescape().unwrap_or_default());
current_text.push_str(&e.decode().unwrap_or_default());
}
},
Ok(Event::End(ref e)) => {
@@ -879,7 +879,7 @@ impl WebDavAdapter {
},
Ok(Event::Text(e)) => {
if in_owner {
owner_text.push_str(&e.unescape().unwrap_or_default());
owner_text.push_str(&e.decode().unwrap_or_default());
}
},
Ok(Event::End(ref e)) => {
-1
View File
@@ -1,6 +1,5 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use std::collections::HashMap;
use crate::domain::entities::calendar::Calendar;
use crate::domain::entities::calendar_event::CalendarEvent;
+9
View File
@@ -65,4 +65,13 @@ pub struct ChangePasswordDto {
#[derive(Debug, Serialize, Deserialize)]
pub struct RefreshTokenDto {
pub refresh_token: String,
}
/// Datos del usuario autenticado actual (para uso en servicios de application)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: String,
pub username: String,
pub email: String,
pub role: String,
}
+60
View File
@@ -3,6 +3,66 @@ use crate::domain::entities::user::User;
use crate::domain::entities::session::Session;
use crate::common::errors::DomainError;
// ============================================================================
// Cryptography Ports - Extracted from Domain to maintain Clean Architecture
// ============================================================================
/// Port for password hashing operations.
///
/// This trait abstracts cryptographic password operations, allowing the domain
/// layer to remain independent of specific hashing implementations (argon2, bcrypt, etc.)
pub trait PasswordHasherPort: Send + Sync + 'static {
/// Hash a plain text password
fn hash_password(&self, password: &str) -> Result<String, DomainError>;
/// Verify a plain text password against a hash
fn verify_password(&self, password: &str, hash: &str) -> Result<bool, DomainError>;
}
/// Claims contained in a JWT token
#[derive(Debug, Clone)]
pub struct TokenClaims {
/// Subject identifier (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
pub jti: String,
/// Username
pub username: String,
/// User email
pub email: String,
/// User role
pub role: String,
}
/// Port for JWT token operations.
///
/// This trait abstracts token generation and validation, allowing the domain
/// layer to remain independent of specific JWT implementations.
pub trait TokenServicePort: Send + Sync + 'static {
/// Generate an access token for a user
fn generate_access_token(&self, user: &User) -> Result<String, DomainError>;
/// Validate a token and extract its claims
fn validate_token(&self, token: &str) -> Result<TokenClaims, DomainError>;
/// Generate a refresh token
fn generate_refresh_token(&self) -> String;
/// Get refresh token expiry in seconds
fn refresh_token_expiry_secs(&self) -> i64;
/// Get refresh token expiry in days
fn refresh_token_expiry_days(&self) -> i64;
}
// ============================================================================
// Storage Ports
// ============================================================================
#[async_trait]
pub trait UserStoragePort: Send + Sync + 'static {
/// Crea un nuevo usuario
@@ -1,8 +1,7 @@
use std::sync::Arc;
use crate::domain::entities::user::{User, UserRole};
use crate::domain::entities::session::Session;
use crate::domain::services::auth_service::AuthService;
use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort};
use crate::application::ports::auth_ports::{UserStoragePort, SessionStoragePort, PasswordHasherPort, TokenServicePort};
use crate::application::dtos::user_dto::{UserDto, RegisterDto, LoginDto, AuthResponseDto, ChangePasswordDto, RefreshTokenDto};
use crate::application::dtos::folder_dto::CreateFolderDto;
use crate::application::ports::inbound::FolderUseCase;
@@ -11,7 +10,8 @@ use crate::common::errors::{DomainError, ErrorKind};
pub struct AuthApplicationService {
user_storage: Arc<dyn UserStoragePort>,
session_storage: Arc<dyn SessionStoragePort>,
auth_service: Arc<AuthService>,
password_hasher: Arc<dyn PasswordHasherPort>,
token_service: Arc<dyn TokenServicePort>,
folder_service: Option<Arc<dyn FolderUseCase>>,
}
@@ -19,12 +19,14 @@ impl AuthApplicationService {
pub fn new(
user_storage: Arc<dyn UserStoragePort>,
session_storage: Arc<dyn SessionStoragePort>,
auth_service: Arc<AuthService>,
password_hasher: Arc<dyn PasswordHasherPort>,
token_service: Arc<dyn TokenServicePort>,
) -> Self {
Self {
user_storage,
session_storage,
auth_service,
password_hasher,
token_service,
folder_service: None,
}
}
@@ -127,11 +129,23 @@ impl AuthApplicationService {
1024 * 1024 * 1024 // 1GB para usuarios normales
};
// Crear usuario
// Validar longitud de password antes de hashear
if dto.password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
"Password debe tener al menos 8 caracteres"
));
}
// Hashear el password usando el servicio de infraestructura
let password_hash = self.password_hasher.hash_password(&dto.password)?;
// Crear usuario con el hash pre-generado
let user = User::new(
dto.username.clone(),
dto.email,
dto.password,
password_hash,
role,
quota,
).map_err(|e| DomainError::new(
@@ -203,13 +217,8 @@ impl AuthApplicationService {
));
}
// Verificar contraseña
let is_valid = user.verify_password(&dto.password)
.map_err(|_| DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Credenciales inválidas"
))?;
// Verificar contraseña usando el hasher inyectado
let is_valid = self.password_hasher.verify_password(&dto.password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
@@ -223,11 +232,10 @@ impl AuthApplicationService {
user.register_login();
self.user_storage.update_user(user.clone()).await?;
// Generar tokens
let access_token = self.auth_service.generate_access_token(&user)
.map_err(DomainError::from)?;
// Generar tokens usando el servicio de tokens inyectado
let access_token = self.token_service.generate_access_token(&user)?;
let refresh_token = self.auth_service.generate_refresh_token();
let refresh_token = self.token_service.generate_refresh_token();
// Guardar sesión
let session = Session::new(
@@ -235,7 +243,7 @@ impl AuthApplicationService {
refresh_token.clone(),
None, // IP (se puede añadir desde la capa HTTP)
None, // User-Agent (se puede añadir desde la capa HTTP)
self.auth_service.refresh_token_expiry_days(),
self.token_service.refresh_token_expiry_days(),
);
self.session_storage.create_session(session).await?;
@@ -246,7 +254,7 @@ impl AuthApplicationService {
access_token,
refresh_token,
token_type: "Bearer".to_string(),
expires_in: self.auth_service.refresh_token_expiry_secs(),
expires_in: self.token_service.refresh_token_expiry_secs(),
})
}
@@ -283,10 +291,9 @@ impl AuthApplicationService {
self.session_storage.revoke_session(session.id()).await?;
// Generar nuevos tokens
let access_token = self.auth_service.generate_access_token(&user)
.map_err(DomainError::from)?;
let access_token = self.token_service.generate_access_token(&user)?;
let new_refresh_token = self.auth_service.generate_refresh_token();
let new_refresh_token = self.token_service.generate_refresh_token();
// Crear nueva sesión
let new_session = Session::new(
@@ -294,7 +301,7 @@ impl AuthApplicationService {
new_refresh_token.clone(),
None,
None,
self.auth_service.refresh_token_expiry_days(),
self.token_service.refresh_token_expiry_days(),
);
self.session_storage.create_session(new_session).await?;
@@ -304,7 +311,7 @@ impl AuthApplicationService {
access_token,
refresh_token: new_refresh_token,
token_type: "Bearer".to_string(),
expires_in: self.auth_service.refresh_token_expiry_secs(),
expires_in: self.token_service.refresh_token_expiry_secs(),
})
}
@@ -342,13 +349,8 @@ impl AuthApplicationService {
// Obtener usuario
let mut user = self.user_storage.get_user_by_id(user_id).await?;
// Verificar contraseña actual
let is_valid = user.verify_password(&dto.current_password)
.map_err(|_| DomainError::new(
ErrorKind::AccessDenied,
"Auth",
"Contraseña actual incorrecta"
))?;
// Verificar contraseña actual usando el hasher inyectado
let is_valid = self.password_hasher.verify_password(&dto.current_password, user.password_hash())?;
if !is_valid {
return Err(DomainError::new(
@@ -358,13 +360,18 @@ impl AuthApplicationService {
));
}
// Actualizar contraseña
user.update_password(dto.new_password.clone())
.map_err(|e| DomainError::new(
// Validar nueva contraseña
if dto.new_password.len() < 8 {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"User",
format!("Error al cambiar contraseña: {}", e)
))?;
"Password debe tener al menos 8 caracteres"
));
}
// Hashear nueva contraseña y actualizar usuario
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
user.update_password_hash(new_hash);
// Guardar usuario actualizado
self.user_storage.update_user(user).await?;
+2 -51
View File
@@ -1,12 +1,11 @@
use std::sync::Arc;
use thiserror::Error;
use futures::{future::join_all, Future};
use tokio::sync::Semaphore;
use tracing::{info, error};
use tracing::info;
use thiserror::Error;
use crate::application::services::file_service::FileService;
use crate::application::services::folder_service::FolderService;
use crate::domain::services::path_service::StoragePath;
use crate::common::errors::DomainError;
use crate::common::config::AppConfig;
use crate::application::ports::inbound::FolderUseCase;
@@ -15,7 +14,6 @@ use crate::application::dtos::folder_dto::FolderDto;
/// Errores específicos para operaciones por lotes
#[derive(Debug, Error)]
#[allow(dead_code)]
pub enum BatchOperationError {
#[error("Error de dominio: {0}")]
Domain(#[from] DomainError),
@@ -59,52 +57,6 @@ pub struct BatchStats {
pub max_concurrency: usize,
}
/// Tipo de entidad para operaciones por lotes
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum EntityType {
File,
Folder,
}
/// Tipo de operación por lotes
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum BatchOperationType {
Create,
Read,
Update,
Delete,
Copy,
Move,
}
/// Identificador para una entidad (ID o ruta)
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum EntityIdentifier {
Id(String),
Path(StoragePath),
}
impl EntityIdentifier {
#[allow(dead_code)]
pub fn as_id(&self) -> Option<&str> {
match self {
EntityIdentifier::Id(id) => Some(id),
_ => None,
}
}
#[allow(dead_code)]
pub fn as_path(&self) -> Option<&StoragePath> {
match self {
EntityIdentifier::Path(path) => Some(path),
_ => None,
}
}
}
/// Servicio de operaciones por lotes
pub struct BatchOperationService {
file_service: Arc<FileService>,
@@ -494,7 +446,6 @@ impl BatchOperationService {
}
/// Operación genérica de lote para cualquier tipo de función asíncrona
#[allow(dead_code)]
pub async fn generic_batch_operation<T, F, Fut>(
&self,
items: Vec<T>,
@@ -1,14 +1,12 @@
use std::sync::Arc;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::application::dtos::calendar_dto::{
CalendarDto, CalendarEventDto, CreateCalendarDto, UpdateCalendarDto,
CreateEventDto, UpdateEventDto, CreateEventICalDto
};
use crate::application::ports::calendar_ports::{CalendarStoragePort, CalendarUseCase};
use crate::interfaces::middleware::auth::CurrentUser;
use crate::common::errors::{DomainError, ErrorKind};
pub struct CalendarService {
+2 -3
View File
@@ -9,12 +9,11 @@ use crate::application::dtos::address_book_dto::{
};
use crate::application::dtos::contact_dto::{
ContactDto, CreateContactDto, UpdateContactDto, CreateContactVCardDto,
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto,
EmailDto, PhoneDto, AddressDto
ContactGroupDto, CreateContactGroupDto, UpdateContactGroupDto, GroupMembershipDto
};
use crate::application::ports::carddav_ports::{AddressBookUseCase, ContactUseCase};
use crate::application::ports::storage_ports::StorageUseCase;
use crate::common::errors::{DomainError, ErrorContext};
use crate::common::errors::DomainError;
use crate::domain::entities::contact::{AddressBook, Contact, ContactGroup, Email, Phone, Address};
use crate::domain::repositories::address_book_repository::AddressBookRepository;
use crate::domain::repositories::contact_repository::{ContactRepository, ContactGroupRepository};
@@ -16,13 +16,6 @@ impl FileManagementService {
pub fn new(file_repository: Arc<dyn FileWritePort>) -> Self {
Self { file_repository }
}
/// Creates a stub for testing
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub())
}
}
}
#[async_trait]
@@ -18,13 +18,6 @@ impl FileRetrievalService {
pub fn new(file_repository: Arc<dyn FileReadPort>) -> Self {
Self { file_repository }
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub())
}
}
}
#[async_trait]
@@ -48,14 +48,6 @@ impl FileUploadService {
self.storage_usage_service = Some(storage_usage_service);
self
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()),
storage_usage_service: None,
}
}
}
#[async_trait]
@@ -23,14 +23,6 @@ impl AppFileUseCaseFactory {
file_write_repository,
}
}
/// Crea un stub para pruebas
pub fn default_stub() -> Self {
Self {
file_read_repository: Arc::new(crate::infrastructure::repositories::FileFsReadRepository::default_stub()),
file_write_repository: Arc::new(crate::infrastructure::repositories::FileFsWriteRepository::default_stub()),
}
}
}
impl FileUseCaseFactory for AppFileUseCaseFactory {
@@ -51,7 +51,6 @@ impl I18nApplicationService {
}
/// Load translations for all available locales
#[allow(dead_code)]
pub async fn load_all_translations(&self) -> Vec<(Locale, I18nResult<()>)> {
let locales = self.i18n_service.available_locales().await;
let mut results = Vec::new();
@@ -70,7 +69,6 @@ impl I18nApplicationService {
}
/// Check if a locale is supported
#[allow(dead_code)]
pub async fn is_supported(&self, locale: Locale) -> bool {
self.i18n_service.is_supported(locale).await
}
+14 -12
View File
@@ -7,8 +7,8 @@ use thiserror::Error;
use crate::domain::entities::folder::Folder;
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryError};
use crate::domain::repositories::file_repository::FileRepositoryError;
use crate::domain::services::path_service::{PathService, StoragePath};
use crate::application::ports::outbound::IdMappingPort;
use crate::domain::services::path_service::StoragePath;
use crate::application::ports::outbound::{IdMappingPort, StoragePort};
/// Errores específicos del mediador de almacenamiento
#[derive(Debug, Error)]
@@ -99,12 +99,12 @@ pub trait StorageMediator: Send + Sync + 'static {
/// Implementación concreta del mediador de almacenamiento
pub struct FileSystemStorageMediator {
pub folder_repository: Arc<dyn FolderRepository>,
pub path_service: Arc<PathService>,
pub path_service: Arc<dyn StoragePort>,
pub id_mapping: Arc<dyn IdMappingPort>,
}
impl FileSystemStorageMediator {
pub fn new(folder_repository: Arc<dyn FolderRepository>, path_service: Arc<PathService>, id_mapping: Arc<dyn IdMappingPort>) -> Self {
pub fn new(folder_repository: Arc<dyn FolderRepository>, path_service: Arc<dyn StoragePort>, id_mapping: Arc<dyn IdMappingPort>) -> Self {
Self { folder_repository, path_service, id_mapping }
}
@@ -116,7 +116,7 @@ impl FileSystemStorageMediator {
/// Overload para implementar inicialización diferida con repository placeholder
pub fn new_with_lazy_folder(
_folder_repository: Arc<RwLock<Option<Arc<dyn FolderRepository>>>>,
path_service: Arc<PathService>,
path_service: Arc<dyn StoragePort>,
id_mapping: Arc<dyn IdMappingPort>
) -> Self {
// Create temporary stub repository
@@ -208,16 +208,18 @@ impl FolderRepository for FolderRepositoryStub {
}
/// Stub implementation for initialization dependency issues
pub struct StubStorageMediator {
#[allow(dead_code)]
_path_service: Arc<PathService>,
}
/// This is a minimal stub that doesn't require any infrastructure dependencies
pub struct StubStorageMediator;
impl StubStorageMediator {
pub fn new() -> Self {
let root_path = PathBuf::from("/tmp");
let path_service = Arc::new(PathService::new(root_path));
Self { _path_service: path_service }
Self
}
}
impl Default for StubStorageMediator {
fn default() -> Self {
Self::new()
}
}
@@ -3,7 +3,7 @@ use async_trait::async_trait;
use tokio::task;
use crate::common::errors::DomainError;
use crate::application::ports::auth_ports::UserStoragePort;
use crate::domain::repositories::file_repository::FileRepository;
use crate::application::ports::outbound::FileStoragePort;
use crate::application::ports::storage_ports::StorageUsagePort;
use tracing::{info, error, debug};
@@ -14,14 +14,14 @@ use tracing::{info, error, debug};
* is using and updating this information in the user records.
*/
pub struct StorageUsageService {
file_repository: Arc<dyn FileRepository>,
file_repository: Arc<dyn FileStoragePort>,
user_repository: Arc<dyn UserStoragePort>,
}
impl StorageUsageService {
/// Creates a new storage usage service
pub fn new(
file_repository: Arc<dyn FileRepository>,
file_repository: Arc<dyn FileStoragePort>,
user_repository: Arc<dyn UserStoragePort>,
) -> Self {
Self {
@@ -90,7 +90,7 @@ impl StorageUsageService {
async fn calculate_folder_size(&self, folder_id: &str) -> Result<i64, DomainError> {
// Implementation with explicit boxing to handle recursion in async functions
async fn inner_calculate_size(
repo: Arc<dyn FileRepository>,
repo: Arc<dyn FileStoragePort>,
folder_id: &str,
) -> Result<i64, DomainError> {
let mut total_size: i64 = 0;
@@ -37,7 +37,6 @@ impl StorageTransaction {
}
/// Añade una operación sin rollback (para limpieza o logging)
#[allow(dead_code)]
pub fn add_finalizer<F>(&mut self, finalizer: F)
where
F: Future<Output = Result<(), DomainError>> + Send + 'static,