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
@@ -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;