feat: server-authoritative case window via users.toml

The /api/oneliners time window is now read per-user from users.toml
(window_hours, default 72h). Clients no longer carry a window:
client.toml oneliner_window_hours, SyncConfig.window_hours, the
?hours=N query param, and the render_case_list cutoff filter are
gone. ETag suffix keeps the effective hours so an admin edit to
users.toml invalidates client caches on the next request.
OnelinersResponse.window_hours stays in the wire format, but now
exists solely to anchor client reconciliation.
This commit is contained in:
2026-04-21 16:48:01 +02:00
parent c8186c9f88
commit 3218fc62bd
24 changed files with 167 additions and 132 deletions
+18
View File
@@ -22,6 +22,9 @@ pub struct AuthenticatedUser {
pub slug: String,
pub role: String,
pub data_dir: PathBuf,
/// Copied from the matching entry in `users.toml` — the single
/// source of truth for how many hours of history clients see.
pub window_hours: u32,
}
impl AuthenticatedUser {
@@ -61,6 +64,7 @@ where
slug: slug.clone(),
role: user.role.clone(),
data_dir,
window_hours: user.window_hours,
})
}
}
@@ -71,6 +75,9 @@ pub struct AuthenticatedWebUser {
pub slug: String,
pub role: String,
pub data_dir: PathBuf,
/// Mirrors [`AuthenticatedUser::window_hours`] so the same policy
/// is available to web handlers without a second config lookup.
pub window_hours: u32,
}
impl AuthenticatedWebUser {
@@ -124,10 +131,21 @@ where
.ok_or_else(|| AppError::Redirect("/web/login".into()))?;
let data_dir = config.data_path.join(&session.slug);
// Look up the TOML user record for the policy fields the session
// does not carry. A missing record can only happen if users.toml
// was edited while a session was still live; fall back to the
// serde default so the request stays functional.
let window_hours = config
.users
.iter()
.find(|u| u.slug == session.slug)
.map(|u| u.window_hours)
.unwrap_or(72);
Ok(Self {
slug: session.slug.clone(),
role: session.role.clone(),
data_dir,
window_hours,
})
}
}