diff --git a/src/main.rs b/src/main.rs index d7ac49d..3e91afb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,17 +46,29 @@ struct AppState { struct WebhookSecret { name: String, fields: HashMap, + #[serde(default)] + expires: Option, #[serde(skip_deserializing)] #[serde(default)] received_at: String, } +#[derive(Clone, Copy, PartialEq, Serialize)] +enum SecretSource { + Kubernetes, + Webhook, +} + #[derive(Serialize)] struct SecretData { name: String, data: Vec<(String, String)>, + source: SecretSource, #[serde(skip_serializing_if = "Option::is_none")] received_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + expires_at: Option, + expired: bool, } #[derive(Template)] @@ -72,6 +84,49 @@ struct SecretQuery { field: String, } +fn parse_duration(s: &str) -> Option { + let s = s.trim(); + if s.is_empty() { + return None; + } + + let (num_str, unit) = if s.ends_with('m') { + (&s[..s.len()-1], 'm') + } else if s.ends_with('h') { + (&s[..s.len()-1], 'h') + } else { + return None; + }; + + let num: i64 = num_str.parse().ok()?; + match unit { + 'm' => Some(chrono::Duration::minutes(num)), + 'h' => Some(chrono::Duration::hours(num)), + _ => None, + } +} + +fn calculate_expiry(received_at: &str, expires: &Option) -> (Option, bool) { + let duration = match expires { + Some(exp) => match parse_duration(exp) { + Some(d) => d, + None => return (None, false), + }, + None => return (None, false), + }; + + let received = match chrono::NaiveDateTime::parse_from_str(received_at, "%Y-%m-%d %H:%M:%S UTC") { + Ok(dt) => dt, + Err(_) => return (None, false), + }; + + let expires_at = received + duration; + let now = chrono::Utc::now().naive_utc(); + let expired = now > expires_at; + + (Some(expires_at.format("%Y-%m-%d %H:%M:%S UTC").to_string()), expired) +} + async fn read_secrets(state: &AppState) -> Result> { let secrets_api: Api = Api::namespaced(state.client.clone(), &state.namespace); let mut result = Vec::new(); @@ -97,7 +152,10 @@ async fn read_secrets(state: &AppState) -> Result> { result.push(SecretData { name: secret_name.clone(), data: data_pairs, + source: SecretSource::Kubernetes, received_at: None, + expires_at: None, + expired: false, }); } Err(e) => { @@ -105,7 +163,10 @@ async fn read_secrets(state: &AppState) -> Result> { result.push(SecretData { name: secret_name.clone(), data: vec![("error".to_string(), format!("Failed to read: {}", e))], + source: SecretSource::Kubernetes, received_at: None, + expires_at: None, + expired: false, }); } } @@ -141,10 +202,15 @@ async fn index_handler(State(state): State>) -> impl IntoResponse .collect(); data_pairs.sort_by(|a, b| a.0.cmp(&b.0)); + let (expires_at, expired) = calculate_expiry(&webhook_secret.received_at, &webhook_secret.expires); + all_secrets.push(SecretData { name: webhook_secret.name.clone(), data: data_pairs, + source: SecretSource::Webhook, received_at: Some(webhook_secret.received_at.clone()), + expires_at, + expired, }); } } diff --git a/templates/index.html b/templates/index.html index 0d98157..19eea79 100644 --- a/templates/index.html +++ b/templates/index.html @@ -21,12 +21,50 @@ padding: 15px; margin-bottom: 10px; border-radius: 4px; + position: relative; + } + .secret.expired { + border-color: #e74c3c; + background: #fdf2f2; + } + .secret-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; } .secret-name { font-weight: bold; - margin-bottom: 10px; font-size: 16px; } + .source-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + } + .source-badge.k8s { + background: #326ce5; + color: white; + } + .source-badge.webhook { + background: #6c5ce7; + color: white; + } + .expired-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + background: #e74c3c; + color: white; + } .data-item { display: flex; align-items: center; @@ -114,8 +152,21 @@ {% else %} {% for secret in secrets %} -
-
{{ secret.name }}
+
+
+
{{ secret.name }}
+ {% match secret.source %} + {% when SecretSource::Kubernetes %} + ☸ K8s + {% when SecretSource::Webhook %} + ⚡ Webhook + {% endmatch %} + {% if secret.expired %} + {% if let Some(expires) = secret.expires_at %} + ⚠ Expired at {{ expires }} + {% endif %} + {% endif %} +
{% for (key, value) in secret.data %}
{{ key }}: @@ -132,7 +183,7 @@
{% endfor %} {% if let Some(received) = secret.received_at %} -
Received: {{ received }}
+
Received: {{ received }}{% if let Some(expires) = secret.expires_at %}{% if !secret.expired %} · Expires: {{ expires }}{% endif %}{% endif %}
{% endif %}
{% endfor %}