From ad07a5abdab85053cab5ecc954a66b853522d126 Mon Sep 17 00:00:00 2001 From: Dionisio Date: Thu, 12 Feb 2026 14:45:52 +0100 Subject: [PATCH] fix: auto-apply DB schema on fresh install and fix admin registration (#81) - Auto-apply schema.sql when database tables don't exist (embedded in binary) - Handle missing tables gracefully during admin registration (treat as fresh install) - Fix docker-compose depends_on to wait for postgres healthcheck - Rename personal folder from 'Mi Carpeta' to 'My Folder' with backward compat - Translate remaining Spanish messages to English --- Cargo.lock | 2 +- docker-compose.yml | 3 ++- .../services/auth_application_service.rs | 27 ++++++++++++------- .../services/file_upload_service.rs | 11 +++++--- src/application/services/recent_service.rs | 2 +- .../services/storage_usage_service.rs | 11 ++++---- src/infrastructure/db.rs | 22 +++++++++++++-- .../repositories/pg/user_pg_repository.rs | 2 +- .../services/password_hasher.rs | 4 +-- 9 files changed, 58 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90551c14..07cc4c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1686,7 +1686,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oxicloud" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "argon2", diff --git a/docker-compose.yml b/docker-compose.yml index b10a840e..e83b675f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,8 @@ services: networks: - oxicloud depends_on: - - postgres + postgres: + condition: service_healthy environment: - "OXICLOUD_DB_CONNECTION_STRING=postgres://postgres:postgres@postgres/oxicloud" - "DATABASE_URL=postgres://postgres:postgres@postgres/oxicloud" diff --git a/src/application/services/auth_application_service.rs b/src/application/services/auth_application_service.rs index 1940ebe0..e2075b99 100644 --- a/src/application/services/auth_application_service.rs +++ b/src/application/services/auth_application_service.rs @@ -179,13 +179,20 @@ impl AuthApplicationService { } }, Err(e) => { - tracing::error!("Error counting admin users: {}", e); - // For security, if we cannot verify, we reject admin creation - return Err(DomainError::new( - ErrorKind::AccessDenied, - "User", - "Creating additional admin users is not allowed" - )); + let err_msg = e.to_string(); + // If the table doesn't exist, this is a fresh install - allow admin creation + if err_msg.contains("does not exist") || err_msg.contains("relation") { + tracing::info!("Database tables not yet ready, treating as fresh install - allowing admin creation"); + // Continue with registration - this is a fresh install + } else { + tracing::error!("Error counting admin users: {}", e); + // For security, if we cannot verify, we reject admin creation + return Err(DomainError::new( + ErrorKind::AccessDenied, + "User", + "Creating additional admin users is not allowed" + )); + } } } } @@ -244,7 +251,7 @@ impl AuthApplicationService { // Create personal folder for the user if let Some(folder_service) = &self.folder_service { - let folder_name = format!("Mi Carpeta - {}", dto.username); + let folder_name = format!("My Folder - {}", dto.username); match folder_service.create_folder(CreateFolderDto { name: folder_name, @@ -572,7 +579,7 @@ impl AuthApplicationService { // 5. Create personal folder for the new admin if folder service is available if let Some(folder_service) = &self.folder_service { - let folder_name = format!("Mi Carpeta - {}", dto.username); + let folder_name = format!("My Folder - {}", dto.username); match folder_service.create_folder(CreateFolderDto { name: folder_name, @@ -951,7 +958,7 @@ impl AuthApplicationService { /// Helper to create a personal folder for a new user async fn create_personal_folder(&self, username: &str, user_id: &str) { if let Some(folder_service) = &self.folder_service { - let folder_name = format!("Mi Carpeta - {}", username); + let folder_name = format!("My Folder - {}", username); match folder_service.create_folder(CreateFolderDto { name: folder_name.clone(), parent_id: None, diff --git a/src/application/services/file_upload_service.rs b/src/application/services/file_upload_service.rs index a21c37b2..f92d144f 100644 --- a/src/application/services/file_upload_service.rs +++ b/src/application/services/file_upload_service.rs @@ -19,10 +19,15 @@ const WRITE_BEHIND_THRESHOLD: usize = 256 * 1024; /// Helper function to extract username from folder path string fn extract_username_from_path(path: &str) -> Option { - if !path.contains("Mi Carpeta - ") { + // Support both new ("My Folder - ") and legacy ("Mi Carpeta - ") prefixes + let prefix = if path.contains("My Folder - ") { + "My Folder - " + } else if path.contains("Mi Carpeta - ") { + "Mi Carpeta - " + } else { return None; - } - let parts: Vec<&str> = path.split("Mi Carpeta - ").collect(); + }; + let parts: Vec<&str> = path.split(prefix).collect(); if parts.len() <= 1 { return None; } diff --git a/src/application/services/recent_service.rs b/src/application/services/recent_service.rs index 62cddc23..30d4090f 100644 --- a/src/application/services/recent_service.rs +++ b/src/application/services/recent_service.rs @@ -59,7 +59,7 @@ impl RecentItemsUseCase for RecentService { info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id); let removed = self.repo.remove_item(user_id, item_id, item_type).await?; info!( - "{} {} '{}' de recientes para usuario {}", + "{} {} '{}' from recent items for user {}", if removed { "Successfully removed" } else { "Not found" }, item_type, item_id, user_id ); diff --git a/src/application/services/storage_usage_service.rs b/src/application/services/storage_usage_service.rs index 3d6d188a..82dd760c 100644 --- a/src/application/services/storage_usage_service.rs +++ b/src/application/services/storage_usage_service.rs @@ -57,16 +57,17 @@ impl StorageUsageService { let all_folders = self.file_repository.list_files(None).await .map_err(|e| DomainError::internal_error("File repository", e.to_string()))?; - // Find the user's home folder (usually named "Mi Carpeta - {username}") - let home_folder_name = format!("Mi Carpeta - {}", username); - debug!("Looking for home folder: {}", home_folder_name); + // Find the user's home folder (named "My Folder - {username}" or legacy "Mi Carpeta - {username}") + let home_folder_name = format!("My Folder - {}", username); + let legacy_folder_name = format!("Mi Carpeta - {}", username); + debug!("Looking for home folder: {} or {}", home_folder_name, legacy_folder_name); let mut total_usage: i64 = 0; let mut home_folder_id = None; - // Find the home folder ID + // Find the home folder ID (check both new and legacy names) for folder in &all_folders { - if folder.name() == home_folder_name { + if folder.name() == home_folder_name || folder.name() == legacy_folder_name { home_folder_id = Some(folder.id().to_string()); debug!("Found home folder for user {}: ID={}", username, folder.id()); break; diff --git a/src/infrastructure/db.rs b/src/infrastructure/db.rs index 26611954..e5490c7f 100644 --- a/src/infrastructure/db.rs +++ b/src/infrastructure/db.rs @@ -39,11 +39,29 @@ pub async fn create_database_pool(config: &AppConfig) -> Result { Ok(row) => { let tables_exist: bool = row.get(0); if !tables_exist { - tracing::warn!("Database tables do not exist. Please run migrations with: cargo run --bin migrate --features migrations"); + tracing::warn!("Database tables do not exist. Auto-applying schema..."); + let schema_sql = include_str!("../../db/schema.sql"); + match sqlx::raw_sql(schema_sql).execute(&pool).await { + Ok(_) => { + tracing::info!("Database schema applied successfully"); + }, + Err(e) => { + tracing::error!("Failed to auto-apply database schema: {}. You may need to run: psql -f db/schema.sql", e); + } + } } }, Err(_) => { - tracing::warn!("Could not verify migration status. Please run migrations with: cargo run --bin migrate --features migrations"); + tracing::warn!("Could not verify migration status. Attempting to auto-apply schema..."); + let schema_sql = include_str!("../../db/schema.sql"); + match sqlx::raw_sql(schema_sql).execute(&pool).await { + Ok(_) => { + tracing::info!("Database schema applied successfully"); + }, + Err(e) => { + tracing::error!("Failed to auto-apply database schema: {}. You may need to run: psql -f db/schema.sql", e); + } + } } } diff --git a/src/infrastructure/repositories/pg/user_pg_repository.rs b/src/infrastructure/repositories/pg/user_pg_repository.rs index 8d8e8bd2..d2b2c137 100644 --- a/src/infrastructure/repositories/pg/user_pg_repository.rs +++ b/src/infrastructure/repositories/pg/user_pg_repository.rs @@ -54,7 +54,7 @@ impl UserPgRepository { impl UserRepository for UserPgRepository { /// Creates a new user using a transaction async fn create_user(&self, user: User) -> UserRepositoryResult { - // Creamos una copia del usuario para el closure + // Create a copy of the user for the closure let user_clone = user.clone(); with_transaction( diff --git a/src/infrastructure/services/password_hasher.rs b/src/infrastructure/services/password_hasher.rs index 593171c1..3cd77fca 100644 --- a/src/infrastructure/services/password_hasher.rs +++ b/src/infrastructure/services/password_hasher.rs @@ -43,7 +43,7 @@ impl PasswordHasherPort for Argon2PasswordHasher { .map_err(|e| DomainError::new( ErrorKind::InternalError, "PasswordHasher", - format!("Error al generar hash de password: {}", e) + format!("Error generating password hash: {}", e) )) } @@ -52,7 +52,7 @@ impl PasswordHasherPort for Argon2PasswordHasher { .map_err(|e| DomainError::new( ErrorKind::InternalError, "PasswordHasher", - format!("Error al procesar hash: {}", e) + format!("Error processing password hash: {}", e) ))?; Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())