fix: critical bugs from deep audit

- Fix copy_files() data loss: implement real copy_file across full stack
  (FileWritePort, FileManagementUseCase, stubs, service, repository with
  atomic CTE + dedup ref_count increment, batch_operations caller)
- Fix plaintext password in replace_default_admin: hash password via
  PasswordHasherPort before User::new()
- Fix CalendarService hardcoded user_id: unify CalendarUseCase trait with
  explicit user_id parameter on all methods, remove zombie _for_user
  duplicates and hardcoded 'current_user_id', update 20 CalDAV handler
  call sites
- Previous session: migrate DedupService to PostgreSQL (storage.blobs),
  atomic CTEs with compensation for file/folder repository operations
This commit is contained in:
Dionisio
2026-02-14 20:22:19 +01:00
parent 3179e1dd91
commit fac0b5e77b
17 changed files with 2163 additions and 2185 deletions
+20 -20
View File
@@ -171,7 +171,7 @@ async fn handle_propfind(
if path.is_empty() {
// Root CalDAV path — list user's calendars
let calendars = calendar_service
.list_my_calendars_for_user(&user.id)
.list_my_calendars(&user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list calendars: {}", e)))?;
@@ -198,13 +198,13 @@ async fn handle_propfind(
if parts.len() == 1 {
// Calendar collection
let calendar = calendar_service
.get_calendar_for_user(calendar_id, &user.id)
.get_calendar(calendar_id, &user.id)
.await
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
let events = if depth != "0" {
calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.unwrap_or_default()
} else {
@@ -235,7 +235,7 @@ async fn handle_propfind(
let ical_uid = event_file.trim_end_matches(".ics");
let events = calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -295,14 +295,14 @@ async fn handle_report(
CalDavReportType::CalendarQuery { time_range, .. } => {
if let Some((start, end)) = time_range {
calendar_service
.get_events_in_range_for_user(calendar_id, *start, *end, &user.id)
.get_events_in_range(calendar_id, *start, *end, &user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to query events: {}", e))
})?
} else {
calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| {
AppError::internal_error(format!("Failed to list events: {}", e))
@@ -311,7 +311,7 @@ async fn handle_report(
}
CalDavReportType::CalendarMultiget { hrefs, .. } => {
let all_events = calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -321,7 +321,7 @@ async fn handle_report(
.collect()
}
CalDavReportType::SyncCollection { .. } => calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?,
};
@@ -377,7 +377,7 @@ async fn handle_mkcalendar(
};
calendar_service
.create_calendar_for_user(create_dto, &user.id)
.create_calendar(create_dto, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to create calendar: {}", e)))?;
@@ -417,7 +417,7 @@ async fn handle_put(
let existing = if let Some(ref uid) = ical_uid {
let events = calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.unwrap_or_default();
events.into_iter().find(|e| e.ical_uid == *uid)
@@ -428,7 +428,7 @@ async fn handle_put(
if let Some(existing_event) = existing {
// Update existing event — re-create from iCal for full fidelity
calendar_service
.delete_event_for_user(&existing_event.id, &user.id)
.delete_event(&existing_event.id, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update event: {}", e)))?;
@@ -437,7 +437,7 @@ async fn handle_put(
ical_data,
};
let event = calendar_service
.create_event_from_ical_for_user(create_dto, &user.id)
.create_event_from_ical(create_dto, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to recreate event: {}", e)))?;
@@ -453,7 +453,7 @@ async fn handle_put(
};
let event = calendar_service
.create_event_from_ical_for_user(create_dto, &user.id)
.create_event_from_ical(create_dto, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to create event: {}", e)))?;
@@ -492,12 +492,12 @@ async fn handle_get(
if parts.len() < 2 {
// GET on calendar collection
let events = calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
let calendar = calendar_service
.get_calendar_for_user(calendar_id, &user.id)
.get_calendar(calendar_id, &user.id)
.await
.map_err(|e| AppError::not_found(format!("Calendar not found: {}", e)))?;
@@ -515,7 +515,7 @@ async fn handle_get(
let ical_uid = event_file.trim_end_matches(".ics");
let events = calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -602,7 +602,7 @@ async fn handle_delete(
if parts.len() < 2 {
calendar_service
.delete_calendar_for_user(calendar_id, &user.id)
.delete_calendar(calendar_id, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete calendar: {}", e)))?;
} else {
@@ -610,7 +610,7 @@ async fn handle_delete(
let ical_uid = event_file.trim_end_matches(".ics");
let events = calendar_service
.list_events_for_user(calendar_id, None, None, &user.id)
.list_events(calendar_id, None, None, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?;
@@ -620,7 +620,7 @@ async fn handle_delete(
.ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?;
calendar_service
.delete_event_for_user(&event.id, &user.id)
.delete_event(&event.id, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to delete event: {}", e)))?;
}
@@ -675,7 +675,7 @@ async fn handle_proppatch(
if update.name.is_some() || update.description.is_some() || update.color.is_some() {
calendar_service
.update_calendar_for_user(calendar_id, update, &user.id)
.update_calendar(calendar_id, update, &user.id)
.await
.map_err(|e| AppError::internal_error(format!("Failed to update calendar: {}", e)))?;
}
+141 -93
View File
@@ -1,19 +1,19 @@
use std::sync::Arc;
use std::collections::HashMap;
use axum::{
extract::{Path, State, Query},
http::{StatusCode, header, HeaderName, HeaderValue, Response},
response::IntoResponse,
Json,
extract::{Path, Query, State},
http::{HeaderName, HeaderValue, Response, StatusCode, header},
response::IntoResponse,
};
use std::collections::HashMap;
use std::sync::Arc;
use crate::application::services::folder_service::FolderService;
use crate::application::dtos::folder_dto::{CreateFolderDto, RenameFolderDto, MoveFolderDto};
use crate::application::dtos::folder_dto::{CreateFolderDto, MoveFolderDto, RenameFolderDto};
use crate::application::dtos::pagination::PaginationRequestDto;
use crate::common::errors::ErrorKind;
use crate::application::ports::inbound::FolderUseCase;
use crate::application::services::folder_service::FolderService;
use crate::common::di::AppState as GlobalAppState;
use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser};
use crate::common::errors::ErrorKind;
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser};
type AppState = Arc<FolderService>;
@@ -37,14 +37,16 @@ impl FolderHandler {
let home_folder_name = format!("My Folder - {}", auth_user.username);
tracing::info!(
"create_folder: parent_id is None for user '{}', looking up home folder '{}'",
auth_user.username, home_folder_name
auth_user.username,
home_folder_name
);
match service.list_folders(None).await {
Ok(folders) => {
if let Some(home) = folders.iter().find(|f| f.name == home_folder_name) {
tracing::info!(
"create_folder: resolved home folder ID '{}' for user '{}'",
home.id, auth_user.username
home.id,
auth_user.username
);
dto.parent_id = Some(home.id.clone());
} else {
@@ -55,7 +57,10 @@ impl FolderHandler {
}
}
Err(e) => {
tracing::error!("create_folder: failed to list folders for home resolution: {}", e);
tracing::error!(
"create_folder: failed to list folders for home resolution: {}",
e
);
}
}
}
@@ -68,12 +73,12 @@ impl FolderHandler {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
}
}
/// Gets a folder by ID
pub async fn get_folder(
State(service): State<AppState>,
@@ -86,12 +91,12 @@ impl FolderHandler {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
}
}
/// Lists root folders (no parent ID)
/// Non-admin users only see their own home folder.
pub async fn list_root_folders(
@@ -145,18 +150,20 @@ impl FolderHandler {
parent_id: Option<&str>,
) -> axum::response::Response {
match service.list_folders(parent_id).await {
Ok(folders) => {
(StatusCode::OK, Json(folders)).into_response()
},
Ok(folders) => (StatusCode::OK, Json(folders)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
(
status,
Json(serde_json::json!({
"error": err.to_string()
})),
)
.into_response()
}
}
}
@@ -172,35 +179,42 @@ impl FolderHandler {
Ok(folders) => {
// Only filter at root level (parent_id == None)
let filtered = if parent_id.is_none() {
folders.into_iter().filter(|f| {
// Skip hidden/system folders
if f.name.starts_with('.') {
return false;
}
// If it's a user home folder, only show if it belongs to this user
if Self::is_user_home_folder(&f.name) {
return Self::folder_belongs_to_user(&f.name, &auth_user.username);
}
// Non-home folders are visible to everyone
true
}).collect()
folders
.into_iter()
.filter(|f| {
// Skip hidden/system folders
if f.name.starts_with('.') {
return false;
}
// If it's a user home folder, only show if it belongs to this user
if Self::is_user_home_folder(&f.name) {
return Self::folder_belongs_to_user(&f.name, &auth_user.username);
}
// Non-home folders are visible to everyone
true
})
.collect()
} else {
folders
};
(StatusCode::OK, Json(filtered)).into_response()
},
}
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
(
status,
Json(serde_json::json!({
"error": err.to_string()
})),
)
.into_response()
}
}
}
/// Lists folders with pagination support (internal helper)
async fn list_folders_paginated_inner(
service: AppState,
@@ -208,23 +222,25 @@ impl FolderHandler {
parent_id: Option<&str>,
) -> axum::response::Response {
match service.list_folders_paginated(parent_id, &pagination).await {
Ok(paginated_result) => {
(StatusCode::OK, Json(paginated_result)).into_response()
},
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
// Return a JSON error response
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
(
status,
Json(serde_json::json!({
"error": err.to_string()
})),
)
.into_response()
}
}
}
/// Renames a folder
pub async fn rename_folder(
State(service): State<AppState>,
@@ -239,15 +255,19 @@ impl FolderHandler {
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
// Return a proper JSON error response
(status, Json(serde_json::json!({
"error": err.to_string()
}))).into_response()
(
status,
Json(serde_json::json!({
"error": err.to_string()
})),
)
.into_response()
}
}
}
/// Moves a folder to a new parent
pub async fn move_folder(
State(service): State<AppState>,
@@ -262,12 +282,12 @@ impl FolderHandler {
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
}
}
/// Deletes a folder (with trash support)
pub async fn delete_folder(
State(service): State<AppState>,
@@ -281,58 +301,68 @@ impl FolderHandler {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
}
}
/// Deletes a folder with trash functionality
pub async fn delete_folder_with_trash(
State(state): State<GlobalAppState>,
OptionalAuthUser(auth_user): OptionalAuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
let user_id = auth_user.as_ref().map(|u| u.id.as_str()).unwrap_or("anonymous");
let user_id = auth_user
.as_ref()
.map(|u| u.id.as_str())
.unwrap_or("anonymous");
// Check if trash service is available
if let Some(trash_service) = &state.trash_service {
tracing::info!("Moving folder to trash: {}", id);
// Try to move to trash first
match trash_service.move_to_trash(&id, "folder", user_id).await {
Ok(_) => {
tracing::info!("Folder successfully moved to trash: {}", id);
return StatusCode::NO_CONTENT.into_response();
},
}
Err(err) => {
tracing::warn!("Could not move folder to trash, falling back to permanent delete: {}", err);
tracing::warn!(
"Could not move folder to trash, falling back to permanent delete: {}",
err
);
// Fall through to regular delete if trash fails
}
}
}
// Fallback to permanent delete if trash is unavailable or failed
let folder_service = &state.applications.folder_service;
match folder_service.delete_folder(&id).await {
Ok(_) => {
tracing::info!("Folder permanently deleted: {}", id);
StatusCode::NO_CONTENT.into_response()
},
}
Err(err) => {
tracing::error!("Error deleting folder: {}", err);
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": format!("Error deleting folder: {}", err)
}))).into_response()
(
status,
Json(serde_json::json!({
"error": format!("Error deleting folder: {}", err)
})),
)
.into_response()
}
}
}
/// Downloads a folder as a ZIP file
pub async fn download_folder_zip(
State(state): State<GlobalAppState>,
@@ -340,67 +370,85 @@ impl FolderHandler {
Query(_params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
tracing::info!("Downloading folder as ZIP: {}", id);
// Get folder information first to check it exists and get name
let folder_service = &state.applications.folder_service;
match folder_service.get_folder(&id).await {
Ok(folder) => {
tracing::info!("Preparing ZIP for folder: {} ({})", folder.name, id);
// Use ZIP service from DI container
let zip_service = &state.core.zip_service;
// Create the ZIP file
match zip_service.create_folder_zip(&id, &folder.name).await {
Ok(zip_data) => {
tracing::info!("ZIP file created successfully, size: {} bytes", zip_data.len());
tracing::info!(
"ZIP file created successfully, size: {} bytes",
zip_data.len()
);
// Setup headers for download
let filename = format!("{}.zip", folder.name);
let content_disposition = format!("attachment; filename=\"{}\"", filename);
// Build response with the ZIP data
let mut headers = HashMap::new();
headers.insert(header::CONTENT_TYPE.to_string(), "application/zip".to_string());
headers.insert(header::CONTENT_DISPOSITION.to_string(), content_disposition);
headers.insert(header::CONTENT_LENGTH.to_string(), zip_data.len().to_string());
headers.insert(
header::CONTENT_TYPE.to_string(),
"application/zip".to_string(),
);
headers
.insert(header::CONTENT_DISPOSITION.to_string(), content_disposition);
headers.insert(
header::CONTENT_LENGTH.to_string(),
zip_data.len().to_string(),
);
// Build the response
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(zip_data))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
HeaderValue::from_str(&value).unwrap(),
);
}
response
},
}
Err(err) => {
tracing::error!("Error creating ZIP file: {}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": format!("Error creating ZIP file: {}", err)
}))).into_response()
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Error creating ZIP file: {}", err)
})),
)
.into_response()
}
}
},
}
Err(err) => {
tracing::error!("Folder not found: {}", err);
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": format!("Error finding folder: {}", err)
}))).into_response()
(
status,
Json(serde_json::json!({
"error": format!("Error finding folder: {}", err)
})),
)
.into_response()
}
}
}
}
}