fix: resolve file management operations not working (#83)
- Fix rename: context menu was nullifying target reference before rename dialog could use it - Fix delete files/folders: auth extractors were mandatory, causing 401 when auth not configured - Fix view-file: async fetch race condition with context menu cleanup - Fix orphaned ID mappings on file deletion - Fix Authorization: Bearer null headers sent without token - Add OptionalUserId and OptionalAuthUser infallible extractors
This commit is contained in:
@@ -13,7 +13,7 @@ use http_range_header::parse_range_header;
|
||||
use crate::application::ports::compression_ports::{CompressionPort, CompressionLevel};
|
||||
use crate::application::ports::file_ports::OptimizedFileContent;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUserId;
|
||||
use crate::interfaces::middleware::auth::OptionalUserId;
|
||||
|
||||
/**
|
||||
* Type aliases for dependency injection state.
|
||||
@@ -506,22 +506,35 @@ impl FileHandler {
|
||||
///
|
||||
/// All logic (trash fallback, dedup ref-count, hash computation) is handled
|
||||
/// by `FileManagementUseCase::delete_with_cleanup`.
|
||||
///
|
||||
/// When auth is available, uses trash-first deletion; otherwise falls back
|
||||
/// to permanent delete so the endpoint works with or without auth.
|
||||
pub async fn delete_file(
|
||||
State(state): State<GlobalState>,
|
||||
CurrentUserId(user_id): CurrentUserId,
|
||||
OptionalUserId(user_id): OptionalUserId,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let mgmt = &state.applications.file_management_service;
|
||||
|
||||
match mgmt.delete_with_cleanup(&id, &user_id).await {
|
||||
Ok(was_trashed) => {
|
||||
let result = if let Some(uid) = user_id {
|
||||
// Auth available: trash-first with dedup cleanup
|
||||
mgmt.delete_with_cleanup(&id, &uid).await.map(|was_trashed| {
|
||||
if was_trashed {
|
||||
tracing::info!("File moved to trash: {}", id);
|
||||
} else {
|
||||
tracing::info!("File permanently deleted: {}", id);
|
||||
}
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// No auth: permanent delete
|
||||
tracing::warn!("No auth context – permanently deleting file: {}", id);
|
||||
mgmt.delete_file(&id).await.map(|_| {
|
||||
tracing::info!("File permanently deleted (no auth): {}", id);
|
||||
})
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
tracing::error!("Error deleting file: {}", err);
|
||||
let status = if err.to_string().contains("not found") || err.to_string().contains("NotFound") {
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::application::dtos::pagination::PaginationRequestDto;
|
||||
use crate::common::errors::ErrorKind;
|
||||
use crate::application::ports::inbound::FolderUseCase;
|
||||
use crate::common::di::AppState as GlobalAppState;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
use crate::interfaces::middleware::auth::OptionalAuthUser;
|
||||
|
||||
type AppState = Arc<FolderService>;
|
||||
|
||||
@@ -205,15 +205,16 @@ impl FolderHandler {
|
||||
/// Deletes a folder with trash functionality
|
||||
pub async fn delete_folder_with_trash(
|
||||
State(state): State<GlobalAppState>,
|
||||
auth_user: AuthUser,
|
||||
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");
|
||||
// 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", &auth_user.id).await {
|
||||
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();
|
||||
|
||||
@@ -6,7 +6,7 @@ use tracing::{debug, error, warn, instrument};
|
||||
|
||||
// use crate::application::ports::trash_ports::TrashUseCase;
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser};
|
||||
|
||||
/// Gets all items in the trash for the current user
|
||||
#[instrument(skip_all)]
|
||||
@@ -50,11 +50,12 @@ pub async fn get_trash_items(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn move_to_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||
Path((item_type, item_id)): Path<(String, String)>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
let user_id = auth_user.as_ref().map(|u| u.id.as_str()).unwrap_or("anonymous");
|
||||
debug!("Request to move to trash: type={}, id={}, user={}",
|
||||
item_type, item_id, auth_user.id);
|
||||
item_type, item_id, user_id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -64,7 +65,7 @@ pub async fn move_to_trash(
|
||||
})));
|
||||
}
|
||||
};
|
||||
let result = trash_service.move_to_trash(&item_id, &item_type, &auth_user.id).await;
|
||||
let result = trash_service.move_to_trash(&item_id, &item_type, user_id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
@@ -87,11 +88,12 @@ pub async fn move_to_trash(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn move_file_to_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||
Path(item_id): Path<String>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
let user_id = auth_user.as_ref().map(|u| u.id.as_str()).unwrap_or("anonymous");
|
||||
debug!("Request to move file to trash: id={}, user={}",
|
||||
item_id, auth_user.id);
|
||||
item_id, user_id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -103,7 +105,7 @@ pub async fn move_file_to_trash(
|
||||
};
|
||||
|
||||
// Specify that it is a file
|
||||
let result = trash_service.move_to_trash(&item_id, "file", &auth_user.id).await;
|
||||
let result = trash_service.move_to_trash(&item_id, "file", user_id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
@@ -126,11 +128,12 @@ pub async fn move_file_to_trash(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn move_folder_to_trash(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||
Path(item_id): Path<String>,
|
||||
) -> (StatusCode, Json<serde_json::Value>) {
|
||||
let user_id = auth_user.as_ref().map(|u| u.id.as_str()).unwrap_or("anonymous");
|
||||
debug!("Request to move folder to trash: id={}, user={}",
|
||||
item_id, auth_user.id);
|
||||
item_id, user_id);
|
||||
|
||||
let trash_service = match state.trash_service.as_ref() {
|
||||
Some(service) => service,
|
||||
@@ -142,7 +145,7 @@ pub async fn move_folder_to_trash(
|
||||
};
|
||||
|
||||
// Specify that it is a folder
|
||||
let result = trash_service.move_to_trash(&item_id, "folder", &auth_user.id).await;
|
||||
let result = trash_service.move_to_trash(&item_id, "folder", user_id).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::convert::Infallible;
|
||||
use axum::{
|
||||
extract::{State, Request, FromRequestParts},
|
||||
http::{StatusCode, HeaderMap, header, request::Parts},
|
||||
@@ -63,6 +64,45 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional user ID extractor – never fails.
|
||||
/// Yields `Some(id)` when auth middleware ran, `None` otherwise.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OptionalUserId(pub Option<String>);
|
||||
|
||||
impl<S> FromRequestParts<S> for OptionalUserId
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(OptionalUserId(
|
||||
parts.extensions.get::<CurrentUser>().map(|cu| cu.id.clone()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional auth user extractor – never fails.
|
||||
/// Yields `Some(AuthUser)` when auth middleware ran, `None` otherwise.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OptionalAuthUser(pub Option<AuthUser>);
|
||||
|
||||
impl<S> FromRequestParts<S> for OptionalAuthUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Ok(OptionalAuthUser(
|
||||
parts.extensions.get::<CurrentUser>().map(|cu| AuthUser {
|
||||
id: cu.id.clone(),
|
||||
username: cu.username.clone(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Error for authentication operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AuthError {
|
||||
|
||||
Reference in New Issue
Block a user