feat: add per-user retention settings to users.toml
New [user.retention] TOML sub-block with auto_close_days and auto_delete_days. Both default to 0, which disables the respective sweep — a conservative default that preserves current behaviour for existing deployments and lets admins test auto-close in isolation. Only parsing + the test User literals are touched here; the actual lazy sweep consuming these values lands in the next commit.
This commit is contained in:
@@ -12,6 +12,20 @@ pub struct WhisperUserSettings {
|
||||
pub initial_prompt: Option<String>,
|
||||
}
|
||||
|
||||
/// Per-user retention settings from users.toml.
|
||||
///
|
||||
/// `auto_close_days = 0` disables the auto-close sweep (a case is never
|
||||
/// closed for age alone). `auto_delete_days = 0` disables the auto-purge
|
||||
/// sweep (closed cases stay forever until manual purge) — important for
|
||||
/// admin / dev setups where data must survive across restarts.
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
pub struct RetentionUserSettings {
|
||||
#[serde(default)]
|
||||
pub auto_close_days: u32,
|
||||
#[serde(default)]
|
||||
pub auto_delete_days: u32,
|
||||
}
|
||||
|
||||
/// A single user entry from users.toml.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
@@ -21,6 +35,8 @@ pub struct User {
|
||||
pub role: String,
|
||||
#[serde(default)]
|
||||
pub whisper: WhisperUserSettings,
|
||||
#[serde(default)]
|
||||
pub retention: RetentionUserSettings,
|
||||
}
|
||||
|
||||
/// Wrapper for deserializing the [[user]] array from TOML.
|
||||
@@ -258,6 +274,7 @@ mod tests {
|
||||
web_password: String::new(),
|
||||
role: "doctor".into(),
|
||||
whisper: WhisperUserSettings::default(),
|
||||
retention: RetentionUserSettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +294,60 @@ mod tests {
|
||||
assert!(u.whisper.initial_prompt.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_without_retention_block_defaults_to_zero() {
|
||||
let toml_str = r#"
|
||||
[[user]]
|
||||
slug = "a"
|
||||
api_key = "k1"
|
||||
web_password = ""
|
||||
role = "doctor"
|
||||
"#;
|
||||
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
|
||||
let r = &parsed.user[0].retention;
|
||||
// Both zero means "feature disabled", matching the pre-migration
|
||||
// status quo — no case is ever auto-closed or auto-purged.
|
||||
assert_eq!(r.auto_close_days, 0);
|
||||
assert_eq!(r.auto_delete_days, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_with_retention_block() {
|
||||
let toml_str = r#"
|
||||
[[user]]
|
||||
slug = "a"
|
||||
api_key = "k1"
|
||||
web_password = ""
|
||||
role = "doctor"
|
||||
[user.retention]
|
||||
auto_close_days = 7
|
||||
auto_delete_days = 30
|
||||
"#;
|
||||
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
|
||||
let r = &parsed.user[0].retention;
|
||||
assert_eq!(r.auto_close_days, 7);
|
||||
assert_eq!(r.auto_delete_days, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_with_partial_retention_block() {
|
||||
let toml_str = r#"
|
||||
[[user]]
|
||||
slug = "a"
|
||||
api_key = "k1"
|
||||
web_password = ""
|
||||
role = "doctor"
|
||||
[user.retention]
|
||||
auto_close_days = 5
|
||||
"#;
|
||||
let parsed: UsersFile = toml::from_str(toml_str).unwrap();
|
||||
let r = &parsed.user[0].retention;
|
||||
assert_eq!(r.auto_close_days, 5);
|
||||
// Missing field defaults to 0 = disabled — lets admins test auto-close
|
||||
// in isolation without losing data to auto-purge.
|
||||
assert_eq!(r.auto_delete_days, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_user_with_full_whisper_block() {
|
||||
let toml_str = r#"
|
||||
|
||||
@@ -31,6 +31,7 @@ fn make_user(slug: &str) -> User {
|
||||
web_password: bcrypt::hash("s", 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ fn test_config() -> Arc<Config> {
|
||||
web_password: "unused".into(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}],
|
||||
api_keys: HashMap::from([("test-key-123".into(), "dr_test".into())]),
|
||||
..Config::test_default()
|
||||
|
||||
@@ -23,6 +23,7 @@ fn make_user(slug: &str) -> User {
|
||||
web_password: bcrypt::hash("s", 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ fn make_user(slug: &str, password_plain: &str) -> User {
|
||||
web_password: bcrypt::hash(password_plain, 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ fn make_user(slug: &str, password_plain: &str) -> User {
|
||||
web_password: bcrypt::hash(password_plain, 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ fn make_user(slug: &str, password_plain: &str) -> User {
|
||||
web_password: bcrypt::hash(password_plain, 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ fn make_user() -> User {
|
||||
web_password: bcrypt::hash("unused", 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ fn test_config() -> (Arc<Config>, PathBuf) {
|
||||
web_password: "unused".into(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}],
|
||||
api_keys: HashMap::from([(TEST_KEY.into(), TEST_SLUG.into())]),
|
||||
..Config::test_default()
|
||||
|
||||
@@ -37,6 +37,7 @@ fn make_user(slug: &str) -> User {
|
||||
web_password: bcrypt::hash("s", 4).unwrap(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ fn test_config_with_whisper(whisper_url: String) -> Arc<Config> {
|
||||
web_password: "unused".into(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}],
|
||||
whisper_url,
|
||||
whisper_timeout_seconds: 5,
|
||||
|
||||
@@ -22,6 +22,7 @@ fn test_config() -> Arc<Config> {
|
||||
web_password: "unused".into(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}],
|
||||
api_keys: HashMap::from([("test-key-123".into(), "dr_test".into())]),
|
||||
..Config::test_default()
|
||||
|
||||
@@ -21,6 +21,7 @@ fn test_config() -> Arc<Config> {
|
||||
web_password: "unused".into(),
|
||||
role: "doctor".into(),
|
||||
whisper: Default::default(),
|
||||
retention: Default::default(),
|
||||
}],
|
||||
api_keys: HashMap::from([("test-key".into(), "dr_test".into())]),
|
||||
..Config::test_default()
|
||||
|
||||
@@ -8,6 +8,13 @@ slug = "dr_mueller"
|
||||
api_key = "change-me-to-a-secure-key"
|
||||
web_password = "$2b$12$..."
|
||||
role = "doctor"
|
||||
# Optional per-user retention. Both fields default to 0 (feature
|
||||
# disabled). auto_close_days = 7 + auto_delete_days = 30 means cases
|
||||
# without new recordings auto-close after 7 days and are irreversibly
|
||||
# removed 30 days later. Changes require a server restart.
|
||||
[user.retention]
|
||||
auto_close_days = 7
|
||||
auto_delete_days = 30
|
||||
|
||||
[[user]]
|
||||
slug = "dr_schmidt"
|
||||
|
||||
Reference in New Issue
Block a user