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]]
name = "oxicloud"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"anyhow",
"argon2",
+2 -1
View File
@@ -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"
@@ -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,
@@ -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<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;
}
let parts: Vec<&str> = path.split("Mi Carpeta - ").collect();
};
let parts: Vec<&str> = path.split(prefix).collect();
if parts.len() <= 1 {
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);
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
);
@@ -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;
+20 -2
View File
@@ -39,11 +39,29 @@ pub async fn create_database_pool(config: &AppConfig) -> Result<PgPool> {
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);
}
}
}
}
@@ -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<User> {
// Creamos una copia del usuario para el closure
// Create a copy of the user for the closure
let user_clone = user.clone();
with_transaction(
@@ -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())