use async_trait::async_trait; use sqlx::{PgPool, Row}; use std::sync::Arc; use crate::domain::entities::user::{User, UserRole}; use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult}; use crate::application::ports::auth_ports::UserStoragePort; use crate::common::errors::DomainError; pub struct UserPgRepository { pool: Arc, } impl UserPgRepository { pub fn new(pool: Arc) -> Self { Self { pool } } // Método auxiliar para mapear errores SQL a errores de dominio fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError { match err { sqlx::Error::RowNotFound => { UserRepositoryError::NotFound("Usuario no encontrado".to_string()) }, sqlx::Error::Database(db_err) => { if db_err.code().map_or(false, |code| code == "23505") { // Código para violación de unicidad en PostgreSQL UserRepositoryError::AlreadyExists( "Usuario o email ya existe".to_string() ) } else { UserRepositoryError::DatabaseError( format!("Error de base de datos: {}", db_err) ) } }, _ => UserRepositoryError::DatabaseError( format!("Error de base de datos: {}", err) ), } } } #[async_trait] impl UserRepository for UserPgRepository { /// Crea un nuevo usuario async fn create_user(&self, user: User) -> UserRepositoryResult { // Usamos los getters para extraer los valores // Convertimos user.role() a string para pasarlo como texto plano let role_str = user.role().to_string(); // Modificar el SQL para hacer un cast explícito al tipo auth.userrole let result = sqlx::query( r#" INSERT INTO auth.users ( id, username, email, password_hash, role, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active ) VALUES ( $1, $2, $3, $4, $5::auth.userrole, $6, $7, $8, $9, $10, $11 ) RETURNING * "# ) .bind(user.id()) .bind(user.username()) .bind(user.email()) .bind(user.password_hash()) .bind(&role_str) // Convertir a string pero con cast explícito en SQL .bind(user.storage_quota_bytes()) .bind(user.storage_used_bytes()) .bind(user.created_at()) .bind(user.updated_at()) .bind(user.last_login_at()) .bind(user.is_active()) .fetch_one(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(user) // Devolvemos el usuario original por simplicidad } /// Obtiene un usuario por ID async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult { let row = sqlx::query( r#" SELECT id, username, email, password_hash, role, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users WHERE id = $1 "# ) .bind(id) .fetch_one(&*self.pool) .await .map_err(Self::map_sqlx_error)?; // Convert role string to UserRole enum let role_str: String = row.get("role"); let role = match role_str.as_str() { "admin" => UserRole::Admin, _ => UserRole::User, }; Ok(User::from_data( row.get("id"), row.get("username"), row.get("email"), row.get("password_hash"), role, row.get("storage_quota_bytes"), row.get("storage_used_bytes"), row.get("created_at"), row.get("updated_at"), row.get("last_login_at"), row.get("active"), )) } /// Obtiene un usuario por nombre de usuario async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult { let row = sqlx::query( r#" SELECT id, username, email, password_hash, role, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users WHERE username = $1 "# ) .bind(username) .fetch_one(&*self.pool) .await .map_err(Self::map_sqlx_error)?; // Convert role string to UserRole enum let role_str: String = row.get("role"); let role = match role_str.as_str() { "admin" => UserRole::Admin, _ => UserRole::User, }; Ok(User::from_data( row.get("id"), row.get("username"), row.get("email"), row.get("password_hash"), role, row.get("storage_quota_bytes"), row.get("storage_used_bytes"), row.get("created_at"), row.get("updated_at"), row.get("last_login_at"), row.get("active"), )) } /// Obtiene un usuario por correo electrónico async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult { let row = sqlx::query( r#" SELECT id, username, email, password_hash, role, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users WHERE email = $1 "# ) .bind(email) .fetch_one(&*self.pool) .await .map_err(Self::map_sqlx_error)?; // Convert role string to UserRole enum let role_str: String = row.get("role"); let role = match role_str.as_str() { "admin" => UserRole::Admin, _ => UserRole::User, }; Ok(User::from_data( row.get("id"), row.get("username"), row.get("email"), row.get("password_hash"), role, row.get("storage_quota_bytes"), row.get("storage_used_bytes"), row.get("created_at"), row.get("updated_at"), row.get("last_login_at"), row.get("active"), )) } /// Actualiza un usuario existente async fn update_user(&self, user: User) -> UserRepositoryResult { sqlx::query( r#" UPDATE auth.users SET username = $2, email = $3, password_hash = $4, role = $5::auth.userrole, storage_quota_bytes = $6, storage_used_bytes = $7, updated_at = $8, last_login_at = $9, active = $10 WHERE id = $1 "# ) .bind(user.id()) .bind(user.username()) .bind(user.email()) .bind(user.password_hash()) .bind(&user.role().to_string()) // Esto no usa el cast explícito porque el SQL ya lo tiene .bind(user.storage_quota_bytes()) .bind(user.storage_used_bytes()) .bind(user.updated_at()) .bind(user.last_login_at()) .bind(user.is_active()) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(user) } /// Actualiza solo el uso de almacenamiento de un usuario async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> { sqlx::query( r#" UPDATE auth.users SET storage_used_bytes = $2, updated_at = NOW() WHERE id = $1 "# ) .bind(user_id) .bind(usage_bytes) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(()) } /// Actualiza la fecha de último inicio de sesión async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> { sqlx::query( r#" UPDATE auth.users SET last_login_at = NOW(), updated_at = NOW() WHERE id = $1 "# ) .bind(user_id) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(()) } /// Lista usuarios con paginación async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult> { let rows = sqlx::query( r#" SELECT id, username, email, password_hash, role, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users ORDER BY created_at DESC LIMIT $1 OFFSET $2 "# ) .bind(limit) .bind(offset) .fetch_all(&*self.pool) .await .map_err(Self::map_sqlx_error)?; let users = rows.into_iter() .map(|row| { // Convert role string to UserRole enum for each row let role_str: String = row.get("role"); let role = match role_str.as_str() { "admin" => UserRole::Admin, _ => UserRole::User, }; User::from_data( row.get("id"), row.get("username"), row.get("email"), row.get("password_hash"), role, row.get("storage_quota_bytes"), row.get("storage_used_bytes"), row.get("created_at"), row.get("updated_at"), row.get("last_login_at"), row.get("active"), ) }) .collect(); Ok(users) } /// Activa o desactiva un usuario async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> { sqlx::query( r#" UPDATE auth.users SET active = $2, updated_at = NOW() WHERE id = $1 "# ) .bind(user_id) .bind(active) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(()) } /// Cambia la contraseña de un usuario async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> { sqlx::query( r#" UPDATE auth.users SET password_hash = $2, updated_at = NOW() WHERE id = $1 "# ) .bind(user_id) .bind(password_hash) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(()) } /// Cambia el rol de un usuario async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> { // Convertir el rol a string para el binding let role_str = role.to_string(); sqlx::query( r#" UPDATE auth.users SET role = $2::auth.userrole, updated_at = NOW() WHERE id = $1 "# ) .bind(user_id) .bind(&role_str) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(()) } /// Elimina un usuario async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> { sqlx::query( r#" DELETE FROM auth.users WHERE id = $1 "# ) .bind(user_id) .execute(&*self.pool) .await .map_err(Self::map_sqlx_error)?; Ok(()) } } // Implementación del puerto de almacenamiento para la capa de aplicación #[async_trait] impl UserStoragePort for UserPgRepository { async fn create_user(&self, user: User) -> Result { UserRepository::create_user(self, user).await.map_err(DomainError::from) } async fn get_user_by_id(&self, id: &str) -> Result { UserRepository::get_user_by_id(self, id).await.map_err(DomainError::from) } async fn get_user_by_username(&self, username: &str) -> Result { UserRepository::get_user_by_username(self, username).await.map_err(DomainError::from) } async fn get_user_by_email(&self, email: &str) -> Result { UserRepository::get_user_by_email(self, email).await.map_err(DomainError::from) } async fn update_user(&self, user: User) -> Result { UserRepository::update_user(self, user).await.map_err(DomainError::from) } async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError> { UserRepository::update_storage_usage(self, user_id, usage_bytes) .await .map_err(DomainError::from) } async fn list_users(&self, limit: i64, offset: i64) -> Result, DomainError> { UserRepository::list_users(self, limit, offset).await.map_err(DomainError::from) } async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError> { UserRepository::change_password(self, user_id, password_hash) .await .map_err(DomainError::from) } }