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