diff --git a/backend/src/auth/webdav_tokens.rs b/backend/src/auth/webdav_tokens.rs index 7a0610f..8e919ba 100644 --- a/backend/src/auth/webdav_tokens.rs +++ b/backend/src/auth/webdav_tokens.rs @@ -141,14 +141,20 @@ pub fn touch_webdav_token(conn: &mut PgPooledConnection, token_id: Uuid) -> Resu pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result { crate::auth::password::verify_password(secret, token_hash) - .map_err(|err| AppError::internal(format!("failed to verify token: {err}"))) + .map_err(|err| { + tracing::error!(error = ?err, "failed to verify token"); + AppError::internal("failed to verify token") + }) } fn generate_secret() -> Result { let mut buffer = [0u8; TOKEN_SECRET_LENGTH]; OsRng .try_fill_bytes(&mut buffer) - .map_err(|err| AppError::internal(format!("failed to generate token: {err}")))?; + .map_err(|err| { + tracing::error!(error = ?err, "failed to generate token"); + AppError::internal("failed to generate token") + })?; Ok(hex::encode(buffer)) } @@ -156,7 +162,10 @@ fn hash_secret(secret: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let hash = Argon2::default() .hash_password(secret.as_bytes(), &salt) - .map_err(|err| AppError::internal(format!("failed to hash token: {err}")))?; + .map_err(|err| { + tracing::error!(error = ?err, "failed to hash token"); + AppError::internal("failed to hash token") + })?; Ok(hash.to_string()) } diff --git a/backend/src/documents/asset.rs b/backend/src/documents/asset.rs index e2c3d36..07d986b 100644 --- a/backend/src/documents/asset.rs +++ b/backend/src/documents/asset.rs @@ -75,7 +75,10 @@ pub fn build_download_path( .jwt .generate_download_token(document.id, user_id, document.tenant_id) .map(|token| format!("/download/{token}")) - .map_err(|err| AppError::internal(format!("failed to generate download token: {err}"))) + .map_err(|err| { + tracing::error!(error = ?err, "failed to generate download token"); + AppError::internal("failed to generate download token") + }) } pub fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse { diff --git a/backend/src/error.rs b/backend/src/error.rs index 0c272bb..50898d9 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -72,7 +72,10 @@ impl From for AppError { fn from(value: diesel::result::Error) -> Self { match value { diesel::result::Error::NotFound => AppError::not_found(), - _ => AppError::internal(value), + other => { + tracing::error!(error = ?other, "database operation failed"); + AppError::internal("database operation failed") + } } } } diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index 8a45214..35d05ac 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -47,6 +47,7 @@ use crate::schema::{ use crate::state::AppState; use crate::utils::{ db::{no_content, validate_bulk_ids, IntoJsonResponse}, + error::StorageResultExt, http::inline_content_disposition, json::{classify_nullable, NullableValue}, storage_paths::document_version_object_key, @@ -412,7 +413,10 @@ pub async fn list_documents( let ids = quickwit_search(endpoint, index, tenant_id, query_str) .await - .map_err(|err| AppError::internal(format!("quickwit search failed: {err}")))?; + .map_err(|err| { + error!(error = ?err, "quickwit search failed"); + AppError::internal("quickwit search failed") + })?; if ids.is_empty() { return Ok(Json(vec![])); @@ -913,7 +917,10 @@ pub async fn request_document_assets( }), None, ) - .map_err(|err| AppError::internal(format!("failed to enqueue analyze job: {err}")))?; + .map_err(|err| { + error!(error = ?err, "failed to enqueue analyze job"); + AppError::internal("failed to enqueue analyze job") + })?; Ok(StatusCode::ACCEPTED) } @@ -959,7 +966,10 @@ pub async fn reanalyze_selected_documents( }), None, ) - .map_err(|err| AppError::internal(format!("failed to enqueue analyze job: {err}")))?; + .map_err(|err| { + error!(error = ?err, "failed to enqueue analyze job"); + AppError::internal("failed to enqueue analyze job") + })?; queued += 1; } @@ -1048,7 +1058,7 @@ pub async fn get_document_asset( Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS), ) .await - .map_err(|err| AppError::internal(format!("failed to generate asset URL: {err}")))?; + .storage_context("failed to generate asset URL")?; object_responses.push(to_asset_object_response( object, @@ -1178,7 +1188,7 @@ pub async fn download_with_token( Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS), ) .await - .map_err(|err| AppError::internal(format!("failed to generate download URL: {err}")))?; + .storage_context("failed to generate download URL")?; Ok(axum::response::Redirect::temporary(&presigned_url)) } @@ -2035,10 +2045,7 @@ async fn process_upload( content_disposition.clone(), ) .await - .map_err(|err| { - error!(error = %err, key = %s3_key, "failed to store document"); - AppError::internal(format!("failed to store document: {err}")) - })?; + .storage_context("failed to store document")?; let metadata_value = if metadata.is_null() { Value::Object(Default::default()) diff --git a/backend/src/routes/folders.rs b/backend/src/routes/folders.rs index 52d05ba..3ea7c02 100644 --- a/backend/src/routes/folders.rs +++ b/backend/src/routes/folders.rs @@ -164,7 +164,7 @@ pub async fn ensure_folder_path( last_folder = Some(folder); } - last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path".to_string())) + last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path")) })?; Ok(Json(FolderResponse { diff --git a/backend/src/routes/webdav/mod.rs b/backend/src/routes/webdav/mod.rs index c514421..e45bad9 100644 --- a/backend/src/routes/webdav/mod.rs +++ b/backend/src/routes/webdav/mod.rs @@ -25,7 +25,7 @@ use crate::schema::{ user_memberships::dsl as memberships_dsl, users::dsl as users_dsl, }; use crate::state::AppState; -use crate::utils::{http::inline_content_disposition, time::to_http_date}; +use crate::utils::{error::StorageResultExt, http::inline_content_disposition, time::to_http_date}; const REALM: &str = "Papercrate WebDAV"; const DOWNLOAD_URL_TTL_SECONDS: u64 = 300; @@ -108,8 +108,10 @@ async fn handle_propfind( } }; - let body = render_multistatus(&resources) - .map_err(|err| AppError::internal(format!("failed to render WebDAV response: {err}")))?; + let body = render_multistatus(&resources).map_err(|err| { + tracing::error!(error = ?err, "failed to render WebDAV response"); + AppError::internal("failed to render WebDAV response") + })?; let response = Response::builder() .status(multi_status()) @@ -320,7 +322,7 @@ async fn stream_document( Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS), ) .await - .map_err(|err| AppError::internal(format!("failed to presign document download: {err}")))?; + .storage_context("failed to presign document download")?; let client = reqwest::Client::new(); let mut request = client.request(method.clone(), url.clone()); @@ -329,18 +331,17 @@ async fn stream_document( request = request.header(header::RANGE, range.clone()); } - let upstream = request - .send() - .await - .map_err(|err| AppError::internal(format!("failed to fetch document stream: {err}")))?; + let upstream = request.send().await.map_err(|err| { + tracing::error!(error = ?err, "failed to fetch document stream"); + AppError::internal("failed to fetch document stream") + })?; let status = StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); if !(status.is_success() || status == StatusCode::PARTIAL_CONTENT) { - return Err(AppError::internal(format!( - "upstream download returned status {status}" - ))); + tracing::error!(status = %status, "upstream download returned error status"); + return Err(AppError::internal("failed to fetch document stream")); } let mut builder = Response::builder().status(status); @@ -368,9 +369,10 @@ async fn stream_document( builder = builder.header(header::ETAG, format!("\"{}\"", version.id)); if method == Method::HEAD { - return builder - .body(Body::empty()) - .map_err(|err| AppError::internal(format!("failed to build response: {err}"))); + return builder.body(Body::empty()).map_err(|err| { + tracing::error!(error = ?err, "failed to build WebDAV response"); + AppError::internal("failed to build WebDAV response") + }); } let stream = upstream @@ -378,9 +380,10 @@ async fn stream_document( .map(|chunk| chunk.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))); let body = Body::from_stream(stream); - builder - .body(body) - .map_err(|err| AppError::internal(format!("failed to build response: {err}"))) + builder.body(body).map_err(|err| { + tracing::error!(error = ?err, "failed to build WebDAV response"); + AppError::internal("failed to build WebDAV response") + }) } fn authenticate(state: &AppState, headers: &HeaderMap) -> Result, AppError> { diff --git a/backend/src/state.rs b/backend/src/state.rs index 21c1653..6a29e98 100644 --- a/backend/src/state.rs +++ b/backend/src/state.rs @@ -71,12 +71,17 @@ impl AppState { pub(crate) fn db_unscoped(&self) -> AppResult { self.pool .get() - .map_err(|err| AppError::internal(format!("database pool error: {err}"))) + .map_err(|err| { + tracing::error!(error = ?err, "database pool error"); + AppError::internal("database pool error") + }) } pub fn storage_for_tenant(&self, tenant_id: Uuid) -> AppResult { let tenant = self.tenants.get_by_id(tenant_id)?; - TenantStorage::new(self.storage.clone(), &tenant) - .map_err(|err| AppError::internal(format!("tenant storage error: {err}"))) + TenantStorage::new(self.storage.clone(), &tenant).map_err(|err| { + tracing::error!(error = ?err, "tenant storage error"); + AppError::internal("tenant storage error") + }) } } diff --git a/backend/src/tenants.rs b/backend/src/tenants.rs index b525a5a..15b58ed 100644 --- a/backend/src/tenants.rs +++ b/backend/src/tenants.rs @@ -57,7 +57,10 @@ impl TenantService { let mut conn = self .pool .get() - .map_err(|err| AppError::internal(format!("database pool error: {err}")))?; + .map_err(|err| { + tracing::error!(error = ?err, "database pool error"); + AppError::internal("database pool error") + })?; let tenant = loader(&mut conn)?; Ok(tenant) } diff --git a/backend/src/utils/error.rs b/backend/src/utils/error.rs new file mode 100644 index 0000000..6aeca61 --- /dev/null +++ b/backend/src/utils/error.rs @@ -0,0 +1,32 @@ +use diesel::result::Error as DieselError; + +use crate::error::{AppError, AppResult}; + +pub trait DbResultExt { + fn db_context(self, context: &'static str) -> AppResult; +} + +impl DbResultExt for Result { + fn db_context(self, context: &'static str) -> AppResult { + self.map_err(|err| match err { + DieselError::NotFound => AppError::not_found(), + other => { + tracing::error!(error = ?other, "{context}"); + AppError::internal(context) + } + }) + } +} + +pub trait StorageResultExt { + fn storage_context(self, context: &'static str) -> AppResult; +} + +impl StorageResultExt for Result { + fn storage_context(self, context: &'static str) -> AppResult { + self.map_err(|err| { + tracing::error!(error = ?err, "{context}"); + AppError::internal(context) + }) + } +} diff --git a/backend/src/utils/mod.rs b/backend/src/utils/mod.rs index 5b8738e..045e291 100644 --- a/backend/src/utils/mod.rs +++ b/backend/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod bootstrap; pub mod db; +pub mod error; pub mod http; pub mod json; pub mod storage_paths;