use crate::db::{ProjectId, Vault};
use super::error::CoreError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncStatus {
InSync,
Modified,
NeverSealed,
}
#[derive(Debug, Clone)]
pub struct StatusRow {
pub name: String,
pub secret_count: i64,
pub last_modified_at: Option<i64>,
pub sealed_at: Option<i64>,
pub sync_status: SyncStatus,
pub stale_secrets: Vec<String>,
}
pub fn derive_sync_status(last_modified_at: Option<i64>, sealed_at: Option<i64>) -> SyncStatus {
match sealed_at {
None => SyncStatus::NeverSealed,
Some(sealed) => match last_modified_at {
Some(modified) if modified > sealed => SyncStatus::Modified,
_ => SyncStatus::InSync,
},
}
}
fn stale_secret_keys(
vault: &Vault,
env_id: &crate::db::EnvId,
threshold_days: u32,
) -> Result<Vec<String>, CoreError> {
if threshold_days == 0 {
return Ok(Vec::new());
}
let threshold_secs = i64::from(threshold_days) * 86_400;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
Ok(vault
.list_secrets(env_id)?
.into_iter()
.filter(|s| now.saturating_sub(s.updated_at) > threshold_secs)
.map(|s| s.key)
.collect())
}
pub fn get_status_report(
vault: &Vault,
project_id: &ProjectId,
rotation_reminder_days: u32,
) -> Result<Vec<StatusRow>, CoreError> {
let rows = vault
.environment_status(project_id)
.map_err(CoreError::Db)?;
let mut result = Vec::with_capacity(rows.len());
for es in rows {
let sync_status = derive_sync_status(es.last_modified_at, es.sealed_at);
let stale_secrets = stale_secret_keys(vault, &es.id, rotation_reminder_days)?;
result.push(StatusRow {
name: es.name,
secret_count: es.secret_count,
last_modified_at: es.last_modified_at,
sealed_at: es.sealed_at,
sync_status,
stale_secrets,
});
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_sync_status_never_sealed_when_no_marker() {
assert_eq!(
derive_sync_status(None, None),
SyncStatus::NeverSealed,
"no sealed_at must yield NeverSealed"
);
assert_eq!(
derive_sync_status(Some(1_000), None),
SyncStatus::NeverSealed,
"secrets present but no sealed_at must yield NeverSealed"
);
}
#[test]
fn derive_sync_status_modified_when_secret_newer_than_seal() {
assert_eq!(
derive_sync_status(Some(2_000), Some(1_000)),
SyncStatus::Modified,
"secret modified after seal must yield Modified"
);
}
#[test]
fn derive_sync_status_in_sync_when_modified_equals_sealed() {
assert_eq!(
derive_sync_status(Some(1_000), Some(1_000)),
SyncStatus::InSync,
"secret modified at exactly the seal time must yield InSync"
);
}
#[test]
fn derive_sync_status_in_sync_when_secret_older_than_seal() {
assert_eq!(
derive_sync_status(Some(500), Some(1_000)),
SyncStatus::InSync,
"secret modified before seal must yield InSync"
);
}
#[test]
fn derive_sync_status_in_sync_for_empty_env_with_marker() {
assert_eq!(
derive_sync_status(None, Some(1_000)),
SyncStatus::InSync,
"empty env with a sync marker must yield InSync"
);
}
}