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
This commit is contained in:
Dionisio
2026-02-12 14:45:52 +01:00
parent 321fae7dcb
commit ad07a5abda
9 changed files with 58 additions and 26 deletions
Generated
+1 -1
View File
@@ -1686,7 +1686,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "oxicloud" name = "oxicloud"
version = "0.3.0" version = "0.3.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
+2 -1
View File
@@ -31,7 +31,8 @@ services:
networks: networks:
- oxicloud - oxicloud
depends_on: depends_on:
- postgres postgres:
condition: service_healthy
environment: environment:
- "OXICLOUD_DB_CONNECTION_STRING=postgres://postgres:postgres@postgres/oxicloud" - "OXICLOUD_DB_CONNECTION_STRING=postgres://postgres:postgres@postgres/oxicloud"
- "DATABASE_URL=postgres://postgres:postgres@postgres/oxicloud" - "DATABASE_URL=postgres://postgres:postgres@postgres/oxicloud"
@@ -179,13 +179,20 @@ impl AuthApplicationService {
} }
}, },
Err(e) => { Err(e) => {
tracing::error!("Error counting admin users: {}", e); let err_msg = e.to_string();
// For security, if we cannot verify, we reject admin creation // If the table doesn't exist, this is a fresh install - allow admin creation
return Err(DomainError::new( if err_msg.contains("does not exist") || err_msg.contains("relation") {
ErrorKind::AccessDenied, tracing::info!("Database tables not yet ready, treating as fresh install - allowing admin creation");
"User", // Continue with registration - this is a fresh install
"Creating additional admin users is not allowed" } 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 // Create personal folder for the user
if let Some(folder_service) = &self.folder_service { 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 { match folder_service.create_folder(CreateFolderDto {
name: folder_name, name: folder_name,
@@ -572,7 +579,7 @@ impl AuthApplicationService {
// 5. Create personal folder for the new admin if folder service is available // 5. Create personal folder for the new admin if folder service is available
if let Some(folder_service) = &self.folder_service { 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 { match folder_service.create_folder(CreateFolderDto {
name: folder_name, name: folder_name,
@@ -951,7 +958,7 @@ impl AuthApplicationService {
/// Helper to create a personal folder for a new user /// Helper to create a personal folder for a new user
async fn create_personal_folder(&self, username: &str, user_id: &str) { async fn create_personal_folder(&self, username: &str, user_id: &str) {
if let Some(folder_service) = &self.folder_service { 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 { match folder_service.create_folder(CreateFolderDto {
name: folder_name.clone(), name: folder_name.clone(),
parent_id: None, parent_id: None,
@@ -19,10 +19,15 @@ const WRITE_BEHIND_THRESHOLD: usize = 256 * 1024;
/// Helper function to extract username from folder path string /// Helper function to extract username from folder path string
fn extract_username_from_path(path: &str) -> Option<String> { fn extract_username_from_path(path: &str) -> Option<String> {
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; return None;
} };
let parts: Vec<&str> = path.split("Mi Carpeta - ").collect(); let parts: Vec<&str> = path.split(prefix).collect();
if parts.len() <= 1 { if parts.len() <= 1 {
return None; return None;
} }
+1 -1
View File
@@ -59,7 +59,7 @@ impl RecentItemsUseCase for RecentService {
info!("Removing {} '{}' from recent for user {}", item_type, item_id, user_id); 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?; let removed = self.repo.remove_item(user_id, item_id, item_type).await?;
info!( info!(
"{} {} '{}' de recientes para usuario {}", "{} {} '{}' from recent items for user {}",
if removed { "Successfully removed" } else { "Not found" }, if removed { "Successfully removed" } else { "Not found" },
item_type, item_id, user_id item_type, item_id, user_id
); );
@@ -57,16 +57,17 @@ impl StorageUsageService {
let all_folders = self.file_repository.list_files(None).await let all_folders = self.file_repository.list_files(None).await
.map_err(|e| DomainError::internal_error("File repository", e.to_string()))?; .map_err(|e| DomainError::internal_error("File repository", e.to_string()))?;
// Find the user's home folder (usually named "Mi Carpeta - {username}") // Find the user's home folder (named "My Folder - {username}" or legacy "Mi Carpeta - {username}")
let home_folder_name = format!("Mi Carpeta - {}", username); let home_folder_name = format!("My Folder - {}", username);
debug!("Looking for home folder: {}", home_folder_name); 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 total_usage: i64 = 0;
let mut home_folder_id = None; 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 { 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()); home_folder_id = Some(folder.id().to_string());
debug!("Found home folder for user {}: ID={}", username, folder.id()); debug!("Found home folder for user {}: ID={}", username, folder.id());
break; break;
+20 -2
View File
@@ -39,11 +39,29 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
Ok(row) => { Ok(row) => {
let tables_exist: bool = row.get(0); let tables_exist: bool = row.get(0);
if !tables_exist { 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(_) => { 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);
}
}
} }
} }
@@ -54,7 +54,7 @@ impl UserPgRepository {
impl UserRepository for UserPgRepository { impl UserRepository for UserPgRepository {
/// Creates a new user using a transaction /// Creates a new user using a transaction
async fn create_user(&self, user: User) -> UserRepositoryResult<User> { async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
// Creamos una copia del usuario para el closure // Create a copy of the user for the closure
let user_clone = user.clone(); let user_clone = user.clone();
with_transaction( with_transaction(
@@ -43,7 +43,7 @@ impl PasswordHasherPort for Argon2PasswordHasher {
.map_err(|e| DomainError::new( .map_err(|e| DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"PasswordHasher", "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( .map_err(|e| DomainError::new(
ErrorKind::InternalError, ErrorKind::InternalError,
"PasswordHasher", "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()) Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())