refactor(server): extract locate_case_or_404 helper

Consolidate 5 instances of "locate_case + warn! + 404-return"
boilerplate (case_actions.rs x4, user_web.rs x1) into a single
pub(crate) helper in user_web.rs. The helper takes &CaseId (no
String allocation at call sites) plus a context tag that becomes
a structured-log field instead of being baked into the message.

bulk.rs keeps using the underlying locate_case because its miss
semantics differ (silent skip, not 404).
This commit is contained in:
2026-04-19 15:11:09 +02:00
parent 16c6fb2e07
commit 21534ab4d1
2 changed files with 39 additions and 37 deletions
+5 -29
View File
@@ -20,7 +20,7 @@ use crate::case_id::CaseIdPath;
use crate::config::Config;
use crate::error::AppError;
use crate::paths::{read_delete_marker, write_delete_marker, DeleteMarker, DELETE_MARKER};
use crate::routes::user_web::locate_case;
use crate::routes::user_web::locate_case_or_404;
#[derive(Template)]
#[template(path = "document.html")]
@@ -49,13 +49,7 @@ pub async fn handle_analyze_case(
}
let user_root = config.data_path.join(&user.slug);
let case_dir = match locate_case(&user_root, &case_id.to_string()).await {
Some(p) => p,
None => {
warn!(slug = %user.slug, case_id = %case_id, "analyze: case not found");
return Err(AppError::NotFound("Case not found".into()));
}
};
let case_dir = locate_case_or_404(&user_root, &case_id, &user.slug, "analyze").await?;
// Re-analyze: delete the existing document first so the UI stops showing
// the stale "ausgewertet" state while the worker re-computes. Ignore
@@ -103,13 +97,7 @@ pub async fn handle_document_view(
CaseIdPath(case_id): CaseIdPath,
) -> Result<Html<String>, AppError> {
let user_root = config.data_path.join(&user.slug);
let case_dir = match locate_case(&user_root, &case_id.to_string()).await {
Some(p) => p,
None => {
warn!(slug = %user.slug, case_id = %case_id, "document view: case not found");
return Err(AppError::NotFound("Case not found".into()));
}
};
let case_dir = locate_case_or_404(&user_root, &case_id, &user.slug, "document view").await?;
let md = read_document(&case_dir)
.await
@@ -272,13 +260,7 @@ pub async fn handle_delete_case(
CaseIdPath(case_id): CaseIdPath,
) -> Result<Redirect, AppError> {
let user_root = config.data_path.join(&user.slug);
let case_dir = match locate_case(&user_root, &case_id.to_string()).await {
Some(p) => p,
None => {
warn!(slug = %user.slug, case_id = %case_id, "delete: case not found");
return Err(AppError::NotFound("Case not found".into()));
}
};
let case_dir = locate_case_or_404(&user_root, &case_id, &user.slug, "delete").await?;
let marker = DeleteMarker {
batch: uuid::Uuid::new_v4(),
@@ -313,13 +295,7 @@ pub async fn handle_reset_case(
}
let user_root = config.data_path.join(&user.slug);
let case_dir = match locate_case(&user_root, &case_id.to_string()).await {
Some(p) => p,
None => {
warn!(slug = %user.slug, case_id = %case_id, "reset: case not found");
return Err(AppError::NotFound("Case not found".into()));
}
};
let case_dir = locate_case_or_404(&user_root, &case_id, &user.slug, "reset").await?;
reset_case_artefacts(&case_dir)
.await
+34 -8
View File
@@ -11,7 +11,7 @@ use time::{format_description::well_known::Rfc3339, Date, OffsetDateTime};
use crate::analyze::{recovery as analyze_recovery, AnalyzeSender, ANALYSIS_INPUT_FILE, DOCUMENT_FILE};
use crate::auth::AuthenticatedWebUser;
use crate::case_id::CaseIdPath;
use crate::case_id::{CaseId, CaseIdPath};
use crate::config::Config;
use crate::error::AppError;
use crate::routes::web::{scan_recordings, RecordingView};
@@ -278,14 +278,14 @@ pub async fn handle_case_detail(
)
.await;
let case_dir = locate_case_or_404(
&user_root,
&case_id,
&user.slug,
"case detail (possible IDOR probe)",
)
.await?;
let case_id_str = case_id.to_string();
let case_dir = match locate_case(&user_root, &case_id_str).await {
Some(p) => p,
None => {
warn!(slug = %user.slug, case_id = %case_id, "case detail: not found (possible IDOR probe)");
return Err(AppError::NotFound("Case not found".into()));
}
};
info!(slug = %user.slug, case_id = %case_id, "case detail viewed");
let recordings = scan_recordings(&case_dir).await;
@@ -333,6 +333,32 @@ pub(crate) async fn locate_case(user_root: &Path, case_id: &str) -> Option<std::
Some(p)
}
/// Resolve a case directory for a handler, mapping any miss to
/// `AppError::NotFound("Case not found")` — matches the legacy 404 body
/// byte-for-byte. The `context` tag distinguishes log lines from the
/// different handlers (analyze, delete, reset, ...). Callers that need
/// a different not-found policy (silent skip in bulk ops) should keep
/// using `locate_case` directly.
pub(crate) async fn locate_case_or_404(
user_root: &Path,
case_id: &CaseId,
user_slug: &str,
context: &str,
) -> Result<std::path::PathBuf, AppError> {
match locate_case(user_root, &case_id.to_string()).await {
Some(p) => Ok(p),
None => {
warn!(
slug = %user_slug,
case_id = %case_id,
context = %context,
"case not found",
);
Err(AppError::NotFound("Case not found".into()))
}
}
}
/// Scan all of the given user's cases. Returned vec is sorted by most
/// recent recording filename (lexicographic ≈ chronological since
/// timestamps are ISO-8601), newest first. Soft-deleted cases (with