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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user