security: fix vulnerabilities 1-7 from security audit

- Fix #1: Share handler IDOR - enforce owner check on share operations
- Fix #2: list_files_query IDOR - bind folder queries to authenticated user
- Fix #3: Dedup handler IDOR - restrict dedup operations to file owner
- Fix #4: Trash handler OptionalAuthUser - require full AuthUser
- Fix #5: Error info leakage - sanitize 500 error responses
- Fix #6: Chunked upload IDOR - bind upload sessions to user_id,
  add verify_session_owner() check on all session operations
- Fix #7: CSP unsafe-inline removal - migrate all inline scripts,
  styles and event handlers to external files, tighten CSP to
  script-src 'self'; style-src 'self'

New files:
  - static/js/core/theme-init.js (render-blocking theme init)
  - static/js/core/sw-register.js (service worker registration)
  - static/css/views/device-verify.css (extracted inline styles)
  - static/js/views/device-verify/device-verify.js (extracted inline script)
This commit is contained in:
Dionisio
2026-03-05 13:15:34 +01:00
parent fdbb2bf60a
commit b503e08384
38 changed files with 870 additions and 1008 deletions
+42 -12
View File
@@ -149,7 +149,10 @@ pub async fn move_files_batch(
.batch_service
.move_files(request.file_ids, request.target_folder_id, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch move_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into();
@@ -190,7 +193,10 @@ pub async fn copy_files_batch(
.batch_service
.copy_files(request.file_ids, request.target_folder_id, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch copy_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into();
@@ -231,7 +237,10 @@ pub async fn delete_files_batch(
.batch_service
.delete_files(request.file_ids, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch delete_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Create custom response for string IDs
let response = BatchOperationResponse {
@@ -280,7 +289,10 @@ pub async fn delete_folders_batch(
.batch_service
.delete_folders(request.folder_ids, request.recursive, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch delete_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Create custom response for string IDs
let response = BatchOperationResponse {
@@ -336,7 +348,10 @@ pub async fn create_folders_batch(
.batch_service
.create_folders(folders, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch create_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Convert result to DTO
let response: BatchOperationResponse<FolderDto> = result.into();
@@ -377,7 +392,10 @@ pub async fn get_files_batch(
.batch_service
.get_multiple_files(request.file_ids, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch get_files failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Convert result to DTO
let response: BatchOperationResponse<FileDto> = result.into();
@@ -418,7 +436,10 @@ pub async fn get_folders_batch(
.batch_service
.get_multiple_folders(request.folder_ids, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch get_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
// Convert result to DTO
let response: BatchOperationResponse<FolderDto> = result.into();
@@ -497,9 +518,10 @@ pub async fn trash_batch(
);
}
Err(e) => {
tracing::error!("Batch trash_files failed: {}", e);
return Ok((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
Json(serde_json::json!({ "error": "Batch trash operation failed" })),
)
.into_response());
}
@@ -523,9 +545,10 @@ pub async fn trash_batch(
);
}
Err(e) => {
tracing::error!("Batch trash_folders failed: {}", e);
return Ok((
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
Json(serde_json::json!({ "error": "Batch trash operation failed" })),
)
.into_response());
}
@@ -579,7 +602,10 @@ pub async fn move_folders_batch(
.batch_service
.move_folders(request.folder_ids, request.target_folder_id, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch move_folders failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch operation failed".to_string())
})?;
let response: BatchOperationResponse<FolderDto> = result.into();
@@ -616,7 +642,10 @@ pub async fn download_batch(
.batch_service
.download_zip(request.file_ids, request.folder_ids, &auth_user.id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
.map_err(|e| {
tracing::error!("Batch download ZIP failed: {}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Batch download failed".to_string())
})?;
// Read file size for Content-Length before splitting ownership
let file_size = temp_file
@@ -624,9 +653,10 @@ pub async fn download_batch(
.metadata()
.map(|m| m.len())
.map_err(|e| {
tracing::error!("Failed to read temp file metadata: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to read temp file metadata: {}", e),
"Failed to prepare download".to_string(),
)
})?;
@@ -22,7 +22,7 @@ use crate::application::ports::chunked_upload_ports::DEFAULT_CHUNK_SIZE;
use crate::application::ports::file_ports::FileUploadUseCase;
use crate::application::ports::storage_ports::StorageUsagePort;
use crate::common::di::AppState;
use crate::domain::errors::ErrorKind;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
/// Request body for creating an upload session
@@ -146,6 +146,7 @@ impl ChunkedUploadHandler {
match chunked_service
.create_session(
&auth_user.id,
request.filename,
request.folder_id,
content_type,
@@ -157,12 +158,7 @@ impl ChunkedUploadHandler {
Ok(response) => (StatusCode::CREATED, Json(response)).into_response(),
Err(e) => {
tracing::error!("Failed to create upload session: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": e.to_string()
})),
)
AppError::internal_error(format!("Failed to create upload session: {}", e))
.into_response()
}
}
@@ -177,6 +173,7 @@ impl ChunkedUploadHandler {
/// Body: Raw bytes of the chunk
pub async fn upload_chunk(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
Query(params): Query<ChunkUploadParams>,
headers: HeaderMap,
@@ -193,7 +190,7 @@ impl ChunkedUploadHandler {
});
match chunked_service
.upload_chunk(&upload_id, params.chunk_index, body, checksum)
.upload_chunk(&upload_id, &auth_user.id, params.chunk_index, body, checksum)
.await
{
Ok(response) => {
@@ -216,22 +213,7 @@ impl ChunkedUploadHandler {
.unwrap()
.into_response()
}
Err(e) => {
let status = match e.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(
status,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response()
}
Err(e) => AppError::from(e).into_response()
}
}
@@ -240,11 +222,12 @@ impl ChunkedUploadHandler {
/// Returns upload progress and pending chunks
pub async fn get_upload_status(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
) -> impl IntoResponse {
let chunked_service = &state.core.chunked_upload_service;
match chunked_service.get_status(&upload_id).await {
match chunked_service.get_status(&upload_id, &auth_user.id).await {
Ok(status) => Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
@@ -261,13 +244,7 @@ impl ChunkedUploadHandler {
))
.unwrap()
.into_response(),
Err(e) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response(),
Err(e) => AppError::from(e).into_response(),
}
}
@@ -276,6 +253,7 @@ impl ChunkedUploadHandler {
/// Assembles all chunks into the final file and creates the file record
pub async fn complete_upload(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
) -> impl IntoResponse {
let chunked_service = &state.core.chunked_upload_service;
@@ -283,22 +261,10 @@ impl ChunkedUploadHandler {
// Assemble chunks (hash-on-write: SHA-256 computed during assembly)
let (assembled_path, filename, folder_id, content_type, total_size, hash) =
match chunked_service.complete_upload(&upload_id).await {
match chunked_service.complete_upload(&upload_id, &auth_user.id).await {
Ok(result) => result,
Err(e) => {
let status = match e.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::InvalidInput | ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
return (
status,
Json(serde_json::json!({
"error": e.to_string()
})),
)
.into_response();
return AppError::from(e).into_response();
}
};
@@ -323,7 +289,7 @@ impl ChunkedUploadHandler {
{
Ok(file) => {
// Cleanup session
let _ = chunked_service.finalize_upload(&upload_id).await;
let _ = chunked_service.finalize_upload(&upload_id, &auth_user.id).await;
tracing::info!(
"✅ CHUNKED UPLOAD COMPLETE: {} (ID: {}, {} bytes)",
@@ -345,12 +311,7 @@ impl ChunkedUploadHandler {
}
Err(e) => {
tracing::error!("Failed to create file from assembled upload: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to create file: {:?}", e)
})),
)
AppError::internal_error(format!("Failed to create file: {}", e))
.into_response()
}
}
@@ -361,18 +322,14 @@ impl ChunkedUploadHandler {
/// Cancels an in-progress upload and cleans up temp files
pub async fn cancel_upload(
State(state): State<Arc<AppState>>,
auth_user: AuthUser,
Path(upload_id): Path<String>,
) -> impl IntoResponse {
let chunked_service = &state.core.chunked_upload_service;
match chunked_service.cancel_upload(&upload_id).await {
match chunked_service.cancel_upload(&upload_id, &auth_user.id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": e.to_string()
})),
)
Err(e) => AppError::internal_error(format!("Failed to cancel upload: {}", e))
.into_response(),
}
}
+88 -98
View File
@@ -9,6 +9,7 @@ use serde::Serialize;
use crate::application::ports::dedup_ports::DedupResultDto;
use crate::common::di::AppState;
use crate::interfaces::middleware::auth::AuthUser;
use std::sync::Arc;
/// Global application state for dependency injection
@@ -72,14 +73,15 @@ pub struct StatsResponse {
pub struct DedupHandler;
impl DedupHandler {
/// Check if a blob with the given hash already exists
/// Check if the authenticated user already has a file with the given hash.
///
/// This endpoint allows clients to check if uploading a file is necessary
/// by pre-computing the hash client-side and checking against the server.
/// User-scoped: only reveals whether **this user** owns a file that
/// references the blob — never exposes global existence or ref_count.
///
/// GET /api/dedup/check/{hash}
pub async fn check_hash(
State(state): State<GlobalState>,
auth_user: AuthUser,
Path(hash): Path<String>,
) -> impl IntoResponse {
let dedup = &state.core.dedup_service;
@@ -96,35 +98,37 @@ impl DedupHandler {
.into_response();
}
match dedup.get_blob_metadata(&hash).await {
Some(metadata) => {
let response = HashCheckResponse {
exists: true,
hash,
existing_size: Some(metadata.size),
ref_count: Some(metadata.ref_count),
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&response).unwrap()))
.unwrap()
.into_response()
}
None => {
let response = HashCheckResponse {
exists: false,
hash,
existing_size: None,
ref_count: None,
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&response).unwrap()))
.unwrap()
.into_response()
}
// Only reveal whether THIS user has the blob — no global oracle
let user_has_it = dedup.user_owns_blob_reference(&hash, &auth_user.id).await;
if user_has_it {
// Fetch size from metadata (safe — user owns a reference)
let size = dedup.get_blob_metadata(&hash).await.map(|m| m.size);
let response = HashCheckResponse {
exists: true,
hash,
existing_size: size,
ref_count: None, // Never expose global ref_count
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&response).unwrap()))
.unwrap()
.into_response()
} else {
let response = HashCheckResponse {
exists: false,
hash,
existing_size: None,
ref_count: None,
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(&response).unwrap()))
.unwrap()
.into_response()
}
}
@@ -139,6 +143,7 @@ impl DedupHandler {
/// Returns information about whether the content was new or deduplicated.
pub async fn upload_with_dedup(
State(state): State<GlobalState>,
_auth_user: AuthUser,
mut multipart: Multipart,
) -> impl IntoResponse {
let dedup = &state.core.dedup_service;
@@ -222,14 +227,13 @@ impl DedupHandler {
.into_response();
}
Err(e) => {
tracing::error!("❌ Dedup upload failed: {}", e);
tracing::error!("Dedup upload failed: {}", e);
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(format!(
r#"{{"error": "Upload failed: {}"}}"#,
e
)))
.body(Body::from(
r#"{"error": "Upload failed"}"#,
))
.unwrap()
.into_response();
}
@@ -256,7 +260,20 @@ impl DedupHandler {
/// - Total references
/// - Bytes saved
/// - Deduplication ratio
pub async fn get_stats(State(state): State<GlobalState>) -> impl IntoResponse {
pub async fn get_stats(
State(state): State<GlobalState>,
auth_user: AuthUser,
) -> impl IntoResponse {
// Admin-only — global dedup statistics are sensitive infrastructure data
if auth_user.role != "admin" {
return Response::builder()
.status(StatusCode::FORBIDDEN)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"error": "Admin role required"}"#))
.unwrap()
.into_response();
}
let dedup = &state.core.dedup_service;
let stats = dedup.get_stats().await;
@@ -285,14 +302,16 @@ impl DedupHandler {
.into_response()
}
/// Retrieve content by hash
/// Retrieve content by hash (user-scoped).
///
/// GET /api/dedup/blob/{hash}
///
/// Returns the raw content of a blob identified by its SHA-256 hash.
/// Useful for retrieving deduplicated content.
/// Returns the raw content of a blob **only if** the authenticated user
/// owns at least one file that references it. Returns 404 otherwise
/// (does not reveal whether the blob exists globally).
pub async fn get_blob(
State(state): State<GlobalState>,
auth_user: AuthUser,
Path(hash): Path<String>,
) -> impl IntoResponse {
let dedup = &state.core.dedup_service;
@@ -307,6 +326,16 @@ impl DedupHandler {
.into_response();
}
// Verify the user owns at least one file referencing this blob
if !dedup.user_owns_blob_reference(&hash, &auth_user.id).await {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"error": "Blob not found"}"#))
.unwrap()
.into_response();
}
// Get metadata first for content-type
let metadata = dedup.get_blob_metadata(&hash).await;
let content_type = metadata
@@ -345,65 +374,26 @@ impl DedupHandler {
}
}
/// Remove a reference to a blob
///
/// DELETE /api/dedup/blob/{hash}
///
/// Decrements the reference count for a blob. If the reference count
/// reaches zero, the blob is deleted from storage.
pub async fn remove_reference(
State(state): State<GlobalState>,
Path(hash): Path<String>,
) -> impl IntoResponse {
let dedup = &state.core.dedup_service;
// Validate hash format
if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"error": "Invalid hash format"}"#))
.unwrap()
.into_response();
}
match dedup.remove_reference(&hash).await {
Ok(deleted) => {
let message = if deleted {
format!(
r#"{{"success": true, "deleted": true, "message": "Blob {} was deleted (ref_count reached 0)"}}"#,
hash
)
} else {
format!(
r#"{{"success": true, "deleted": false, "message": "Reference removed from blob {}"}}"#,
hash
)
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(message))
.unwrap()
.into_response()
}
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(format!(r#"{{"error": "{}"}}"#, e)))
.unwrap()
.into_response(),
}
}
/// Force recalculation of statistics from disk
///
/// POST /api/dedup/recalculate
///
/// Verifies integrity and returns current statistics.
/// Useful for health checks and auditing.
pub async fn recalculate_stats(State(state): State<GlobalState>) -> impl IntoResponse {
pub async fn recalculate_stats(
State(state): State<GlobalState>,
auth_user: AuthUser,
) -> impl IntoResponse {
// Admin-only — integrity verification is a privileged operation
if auth_user.role != "admin" {
return Response::builder()
.status(StatusCode::FORBIDDEN)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"error": "Admin role required"}"#))
.unwrap()
.into_response();
}
let dedup = &state.core.dedup_service;
// Verify integrity first
@@ -414,13 +404,13 @@ impl DedupHandler {
}
}
Err(e) => {
tracing::error!("Dedup integrity verification failed: {}", e);
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(format!(
r#"{{"error": "Verification failed: {}"}}"#,
e
)))
.body(Body::from(
r#"{"error": "Verification failed"}"#,
))
.unwrap()
.into_response();
}
@@ -42,7 +42,7 @@ pub async fn get_favorites(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to retrieve favorites: {}", err)
"error": "Failed to retrieve favorites"
})),
)
.into_response()
@@ -86,7 +86,7 @@ pub async fn add_favorite(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to add to favorites: {}", err)
"error": "Failed to add to favorites"
})),
)
}
@@ -129,7 +129,7 @@ pub async fn remove_favorite(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to remove from favorites: {}", err)
"error": "Failed to remove from favorites"
})),
)
}
@@ -188,7 +188,7 @@ pub async fn batch_add_favorites(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to batch add favorites: {}", err)
"error": "Failed to batch add favorites"
})),
)
.into_response()
+14 -131
View File
@@ -17,6 +17,7 @@ use crate::application::ports::file_ports::{
use crate::application::ports::storage_ports::StorageUsagePort;
use crate::application::ports::thumbnail_ports::ThumbnailPort;
use crate::common::di::AppState;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
use std::sync::Arc;
@@ -291,13 +292,7 @@ impl FileHandler {
{
Ok(f) => f,
Err(err) => {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({
"error": format!("File not found: {}", err)
})),
)
.into_response();
return AppError::from(err).into_response();
}
};
@@ -331,13 +326,7 @@ impl FileHandler {
.into_response()
}
Err(err) => {
tracing::error!("Thumbnail generation failed: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to generate thumbnail: {}", err)
})),
)
AppError::internal_error(format!("Thumbnail generation failed: {}", err))
.into_response()
}
}
@@ -366,20 +355,7 @@ impl FileHandler {
let file_dto = match retrieval.get_file_owned(&id, &auth_user.id).await {
Ok(f) => f,
Err(err) => {
let status = if err.to_string().contains("not found")
|| err.to_string().contains("NotFound")
{
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
return (
status,
Json(serde_json::json!({
"error": err.to_string()
})),
)
.into_response();
return AppError::from(err).into_response();
}
};
@@ -526,14 +502,7 @@ impl FileHandler {
.into_response(),
},
Err(err) => {
tracing::error!("Error downloading file: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Error reading file: {}", err)
})),
)
.into_response()
AppError::from(err).into_response()
}
}
}
@@ -547,6 +516,7 @@ impl FileHandler {
/// Axum-compatible handler wrapper around [`Self::list_files`].
pub async fn list_files_query(
State(state): State<GlobalState>,
auth_user: AuthUser,
headers: HeaderMap,
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
@@ -554,7 +524,7 @@ impl FileHandler {
tracing::info!("API: Listing files with folder_id: {:?}", folder_id);
let retrieval = &state.applications.file_retrieval_service;
match retrieval.list_files(folder_id).await {
match retrieval.list_files_owned(folder_id, &auth_user.id).await {
Ok(files) => {
// Compute lightweight ETag from max modified_at + count
let max_mod = files.iter().map(|f| f.modified_at).max().unwrap_or(0);
@@ -584,14 +554,7 @@ impl FileHandler {
resp
}
Err(err) => {
tracing::error!("Error listing files: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Error listing files: {}", err)
})),
)
.into_response()
AppError::from(err).into_response()
}
}
}
@@ -664,23 +627,7 @@ impl FileHandler {
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")
{
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
(
status,
Json(serde_json::json!({
"error": format!("Error deleting file: {}", err)
})),
)
.into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -712,25 +659,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service;
match mgmt.rename_file_owned(&id, &auth_user.id, &new_name).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => {
tracing::error!("Error renaming file: {}", err);
let status = if err.to_string().contains("not found")
|| err.to_string().contains("NotFound")
{
StatusCode::NOT_FOUND
} else if err.to_string().contains("already exists") {
StatusCode::CONFLICT
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
(
status,
Json(serde_json::json!({
"error": format!("Error renaming file: {}", err)
})),
)
.into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -750,23 +679,7 @@ impl FileHandler {
.await
{
Ok(file) => (StatusCode::OK, Json(file)).into_response(),
Err(err) => {
tracing::error!("Error moving file: {}", err);
let status = if err.to_string().contains("not found")
|| err.to_string().contains("NotFound")
{
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
(
status,
Json(serde_json::json!({
"error": format!("Error moving file: {}", err)
})),
)
.into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -785,16 +698,7 @@ impl FileHandler {
let mgmt = &state.applications.file_management_service;
match mgmt.move_file_owned(&id, &auth_user.id, folder_id).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => {
tracing::error!("Error moving file: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Error moving file: {}", err)
})),
)
.into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -831,33 +735,12 @@ impl FileHandler {
/// Build error response for DomainError.
fn domain_error_response(err: crate::common::errors::DomainError) -> Response<Body> {
let status = match err.kind {
crate::common::errors::ErrorKind::NotFound => StatusCode::NOT_FOUND,
crate::common::errors::ErrorKind::QuotaExceeded => StatusCode::INSUFFICIENT_STORAGE,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::json!({ "error": format!("Error: {}", err) }).to_string(),
))
.unwrap()
AppError::from(err).into_response()
}
/// Build a quota-specific error response with 507 status and structured body.
fn quota_error_response(err: crate::common::errors::DomainError) -> Response<Body> {
Response::builder()
.status(StatusCode::INSUFFICIENT_STORAGE)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::json!({
"error": err.message,
"error_type": "QuotaExceeded"
})
.to_string(),
))
.unwrap()
AppError::from(err).into_response()
}
/// Build response for cached/small files.
+14 -118
View File
@@ -18,7 +18,7 @@ use crate::application::ports::inbound::FolderUseCase;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::application::services::folder_service::FolderService;
use crate::common::di::AppState as GlobalAppState;
use crate::common::errors::ErrorKind;
use crate::interfaces::errors::AppError;
use crate::interfaces::middleware::auth::AuthUser;
type AppState = Arc<FolderService>;
@@ -69,15 +69,7 @@ impl FolderHandler {
match service.create_folder(dto).await {
Ok(folder) => (StatusCode::CREATED, Json(folder)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -100,18 +92,11 @@ impl FolderHandler {
id,
owner
);
return (StatusCode::NOT_FOUND, "Folder not found".to_string()).into_response();
return AppError::not_found("Folder not found").into_response();
}
(StatusCode::OK, Json(folder)).into_response()
}
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -156,17 +141,7 @@ impl FolderHandler {
.await
{
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,
};
(
status,
Json(serde_json::json!({ "error": err.to_string() })),
)
.into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -183,17 +158,7 @@ impl FolderHandler {
.await
{
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()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -233,7 +198,7 @@ impl FolderHandler {
// Run both queries concurrently — no sequential wait.
let (folders_result, files_result) = tokio::join!(
folder_service.list_folders_for_owner(Some(&id), &auth_user.id),
file_service.list_files(Some(&id))
file_service.list_files_owned(Some(&id), &auth_user.id)
);
match (folders_result, files_result) {
@@ -259,17 +224,7 @@ impl FolderHandler {
.insert(header::ETAG, header::HeaderValue::from_str(&etag).unwrap());
resp
}
(Err(err), _) | (_, 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()
}
(Err(err), _) | (_, Err(err)) => AppError::from(err).into_response()
}
}
@@ -282,22 +237,7 @@ impl FolderHandler {
) -> impl IntoResponse {
match service.rename_folder(&id, dto, &auth_user.id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
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()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -310,15 +250,7 @@ impl FolderHandler {
) -> impl IntoResponse {
match service.move_folder(&id, dto, &auth_user.id).await {
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AlreadyExists => StatusCode::CONFLICT,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -330,14 +262,7 @@ impl FolderHandler {
) -> impl IntoResponse {
match service.delete_folder(&id, &auth_user.id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, err.to_string()).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -376,20 +301,7 @@ impl FolderHandler {
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()
AppError::from(err).into_response()
}
}
}
@@ -487,30 +399,14 @@ impl FolderHandler {
}
Err(err) => {
tracing::error!("Error creating ZIP file: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Error creating ZIP file: {}", err)
})),
)
AppError::internal_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()
AppError::from(err).into_response()
}
}
}
+11 -5
View File
@@ -56,16 +56,22 @@ impl I18nHandler {
(StatusCode::OK, Json(response)).into_response()
}
Err(err) => {
let status = match &err {
I18nError::KeyNotFound(_) => StatusCode::NOT_FOUND,
I18nError::InvalidLocale(_) => StatusCode::BAD_REQUEST,
I18nError::LoadError(_) => StatusCode::INTERNAL_SERVER_ERROR,
let (status, error_msg) = match &err {
I18nError::KeyNotFound(_) => (StatusCode::NOT_FOUND, err.to_string()),
I18nError::InvalidLocale(_) => (StatusCode::BAD_REQUEST, err.to_string()),
I18nError::LoadError(_) => {
tracing::error!("I18n load error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
"Translation loading error".to_string(),
)
}
};
let error = TranslationErrorDto {
key: query.key,
locale: locale.unwrap_or(Locale::default()).as_str().to_string(),
error: err.to_string(),
error: error_msg,
};
(status, Json(error)).into_response()
@@ -37,7 +37,7 @@ pub async fn get_recent_items(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to retrieve recent items: {}", err)
"error": "Failed to retrieve recent items"
})),
)
.into_response()
@@ -83,7 +83,7 @@ pub async fn record_item_access(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to record access: {}", err)
"error": "Failed to record access"
})),
)
.into_response()
@@ -129,7 +129,7 @@ pub async fn remove_from_recent(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to remove from recents: {}", err)
"error": "Failed to remove from recents"
})),
)
.into_response()
@@ -160,7 +160,7 @@ pub async fn clear_recent_items(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to clear recent items: {}", err)
"error": "Failed to clear recent items"
})),
)
.into_response()
@@ -74,7 +74,7 @@ impl SearchHandler {
error!("Search error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": format!("Search error: {}", err) })),
Json(json!({ "error": "Search error" })),
)
.into_response()
}
@@ -115,7 +115,7 @@ impl SearchHandler {
error!("Search error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": format!("Search error: {}", err) })),
Json(json!({ "error": "Search error" })),
)
.into_response()
}
@@ -159,7 +159,7 @@ impl SearchHandler {
error!("Suggestions error: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": format!("Suggestions error: {}", err) })),
Json(json!({ "error": "Suggestions error" })),
)
.into_response()
}
@@ -195,7 +195,7 @@ impl SearchHandler {
error!("Error clearing search cache: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": format!("Error clearing search cache: {}", err) })),
Json(json!({ "error": "Error clearing search cache" })),
)
.into_response()
}
+55 -93
View File
@@ -17,7 +17,8 @@ use crate::{
},
common::errors::ErrorKind,
domain::entities::share::ShareItemType,
interfaces::middleware::auth::OptionalAuthUser,
interfaces::errors::AppError,
interfaces::middleware::auth::AuthUser,
};
#[derive(Debug, Deserialize)]
@@ -36,40 +37,27 @@ pub struct VerifyPasswordRequest {
/// Create a new shared link
pub async fn create_shared_link(
State(share_use_case): State<Arc<ShareService>>,
auth_user: OptionalAuthUser,
auth_user: AuthUser,
Json(dto): Json<CreateShareDto>,
) -> impl IntoResponse {
let user_id = auth_user
.0
.map(|u| u.id)
.unwrap_or_else(|| "anonymous".to_string());
match share_use_case.create_shared_link(&user_id, dto).await {
match share_use_case
.create_shared_link(&auth_user.id, dto)
.await
{
Ok(share) => (StatusCode::CREATED, Json(share)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": err.to_string() }))).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
/// Get information about a specific shared link by ID
pub async fn get_shared_link(
State(share_use_case): State<Arc<ShareService>>,
auth_user: AuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
match share_use_case.get_shared_link(&id).await {
match share_use_case.get_shared_link(&id, &auth_user.id).await {
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": err.to_string() }))).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -77,13 +65,10 @@ pub async fn get_shared_link(
/// Supports optional filtering by item_id + item_type query params.
pub async fn get_user_shares(
State(share_use_case): State<Arc<ShareService>>,
auth_user: OptionalAuthUser,
auth_user: AuthUser,
Query(query): Query<GetSharesQuery>,
) -> impl IntoResponse {
let _user_id = auth_user
.0
.map(|u| u.id)
.unwrap_or_else(|| "anonymous".to_string());
let user_id = &auth_user.id;
// If both item_id and item_type are provided, return shares for that specific item
if let (Some(item_id), Some(item_type_str)) = (&query.item_id, &query.item_type) {
@@ -98,15 +83,11 @@ pub async fn get_user_shares(
}
};
return match share_use_case
.get_shared_links_for_item(item_id, &item_type)
.get_shared_links_for_item(item_id, &item_type, user_id)
.await
{
Ok(shares) => (StatusCode::OK, Json(shares)).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": err.to_string() })),
)
.into_response(),
Err(err) => AppError::from(err).into_response(),
};
}
@@ -115,53 +96,42 @@ pub async fn get_user_shares(
let per_page = query.per_page.unwrap_or(20);
match share_use_case
.get_user_shared_links(&_user_id, page, per_page)
.get_user_shared_links(user_id, page, per_page)
.await
{
Ok(shares) => (StatusCode::OK, Json(shares)).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": err.to_string() })),
)
.into_response(),
Err(err) => AppError::from(err).into_response(),
}
}
/// Update a shared link's properties
pub async fn update_shared_link(
State(share_use_case): State<Arc<ShareService>>,
auth_user: AuthUser,
Path(id): Path<String>,
Json(dto): Json<UpdateShareDto>,
) -> impl IntoResponse {
match share_use_case.update_shared_link(&id, dto).await {
match share_use_case
.update_shared_link(&id, &auth_user.id, dto)
.await
{
Ok(share) => (StatusCode::OK, Json(share)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AccessDenied => StatusCode::FORBIDDEN,
ErrorKind::InvalidInput => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": err.to_string() }))).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
/// Delete a shared link
pub async fn delete_shared_link(
State(share_use_case): State<Arc<ShareService>>,
auth_user: AuthUser,
Path(id): Path<String>,
) -> impl IntoResponse {
match share_use_case.delete_shared_link(&id).await {
match share_use_case
.delete_shared_link(&id, &auth_user.id)
.await
{
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AccessDenied => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": err.to_string() }))).into_response()
}
Err(err) => AppError::from(err).into_response()
}
}
@@ -177,28 +147,24 @@ pub async fn access_shared_item(
match share_use_case.get_shared_link_by_token(&token).await {
Ok(item) => (StatusCode::OK, Json(item)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AccessDenied => {
if err.message.contains("expired") {
StatusCode::GONE // HTTP 410 Gone for expired links
} else if err.message.contains("password") {
return (
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Password required",
"requiresPassword": true
})),
)
.into_response();
} else {
StatusCode::FORBIDDEN
}
// Special handling for share access errors
if err.kind == ErrorKind::AccessDenied {
if err.message.contains("password") {
return (
StatusCode::UNAUTHORIZED,
Json(json!({
"error": "Password required",
"requiresPassword": true
})),
)
.into_response();
}
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": err.to_string() }))).into_response()
if err.message.contains("expired") {
return AppError::new(StatusCode::GONE, err.message, "Expired")
.into_response();
}
}
AppError::from(err).into_response()
}
}
}
@@ -215,20 +181,16 @@ pub async fn verify_shared_item_password(
{
Ok(item) => (StatusCode::OK, Json(item)).into_response(),
Err(err) => {
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
ErrorKind::AccessDenied => {
if err.message.contains("expired") {
StatusCode::GONE
} else if err.message.contains("password") {
StatusCode::UNAUTHORIZED
} else {
StatusCode::FORBIDDEN
}
if err.kind == ErrorKind::AccessDenied {
if err.message.contains("expired") {
return AppError::new(StatusCode::GONE, err.message, "Expired")
.into_response();
}
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(json!({ "error": err.to_string() }))).into_response()
if err.message.contains("password") {
return AppError::unauthorized("Invalid password").into_response();
}
}
AppError::from(err).into_response()
}
}
}
+11 -71
View File
@@ -6,7 +6,7 @@ use tracing::{debug, error, instrument, warn};
use crate::application::ports::trash_ports::TrashUseCase;
use crate::common::di::AppState;
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser};
use crate::interfaces::middleware::auth::AuthUser;
use std::sync::Arc;
/// Gets all items in the trash for the current user
@@ -46,61 +46,7 @@ pub async fn get_trash_items(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error retrieving trash items: {}", e)
})),
)
}
}
}
/// Moves an item (file or folder) to the trash (generic function, not used directly in routes)
#[instrument(skip_all)]
pub async fn move_to_trash(
State(state): State<Arc<AppState>>,
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, user_id
);
let trash_service = match state.trash_service.as_ref() {
Some(service) => service,
None => {
return (
StatusCode::NOT_IMPLEMENTED,
Json(json!({
"error": "Trash feature is not enabled"
})),
);
}
};
let result = trash_service
.move_to_trash(&item_id, &item_type, user_id)
.await;
match result {
Ok(_) => {
debug!("Item moved to trash successfully");
(
StatusCode::OK,
Json(json!({
"success": true,
"message": "Item moved to trash successfully"
})),
)
}
Err(e) => {
error!("Error moving item to trash: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error moving item to trash: {}", e)
"error": "Error retrieving trash items"
})),
)
}
@@ -111,13 +57,10 @@ pub async fn move_to_trash(
#[instrument(skip_all)]
pub async fn move_file_to_trash(
State(state): State<Arc<AppState>>,
OptionalAuthUser(auth_user): OptionalAuthUser,
auth_user: AuthUser,
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");
let user_id = &auth_user.id;
debug!(
"Request to move file to trash: id={}, user={}",
item_id, user_id
@@ -154,7 +97,7 @@ pub async fn move_file_to_trash(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error moving file to trash: {}", e)
"error": "Error moving file to trash"
})),
)
}
@@ -165,13 +108,10 @@ pub async fn move_file_to_trash(
#[instrument(skip_all)]
pub async fn move_folder_to_trash(
State(state): State<Arc<AppState>>,
OptionalAuthUser(auth_user): OptionalAuthUser,
auth_user: AuthUser,
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");
let user_id = &auth_user.id;
debug!(
"Request to move folder to trash: id={}, user={}",
item_id, user_id
@@ -210,7 +150,7 @@ pub async fn move_folder_to_trash(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error moving folder to trash: {}", e)
"error": "Error moving folder to trash"
})),
)
}
@@ -271,7 +211,7 @@ pub async fn restore_from_trash(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error restoring item from trash: {}", e)
"error": "Error restoring item from trash"
})),
)
}
@@ -334,7 +274,7 @@ pub async fn delete_permanently(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error deleting item permanently: {}", e)
"error": "Error deleting item permanently"
})),
)
}
@@ -378,7 +318,7 @@ pub async fn empty_trash(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Error emptying trash: {}", e)
"error": "Error emptying trash"
})),
)
}
@@ -394,6 +394,7 @@ pub async fn get_editor_url(
AuthUser {
id: user_id,
username,
..
}: AuthUser,
Query(params): Query<EditorUrlParams>,
State(state): State<WopiState>,
+3 -4
View File
@@ -288,10 +288,9 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
"/blob/{hash}",
get(super::handlers::dedup_handler::DedupHandler::get_blob),
)
.route(
"/blob/{hash}",
delete(super::handlers::dedup_handler::DedupHandler::remove_reference),
)
// NOTE: remove_reference is intentionally NOT exposed as a public
// endpoint — ref_count management is an internal concern handled
// automatically when files are deleted via the file API.
.route(
"/recalculate",
post(super::handlers::dedup_handler::DedupHandler::recalculate_stats),