Adjusted UI template. Added expiration for webhook secrets

This commit is contained in:
Ultradesu
2026-01-07 13:39:46 +00:00
parent e48a55c19e
commit 46cb37d4b0
2 changed files with 121 additions and 4 deletions

View File

@@ -46,17 +46,29 @@ struct AppState {
struct WebhookSecret { struct WebhookSecret {
name: String, name: String,
fields: HashMap<String, String>, fields: HashMap<String, String>,
#[serde(default)]
expires: Option<String>,
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
#[serde(default)] #[serde(default)]
received_at: String, received_at: String,
} }
#[derive(Clone, Copy, PartialEq, Serialize)]
enum SecretSource {
Kubernetes,
Webhook,
}
#[derive(Serialize)] #[derive(Serialize)]
struct SecretData { struct SecretData {
name: String, name: String,
data: Vec<(String, String)>, data: Vec<(String, String)>,
source: SecretSource,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
received_at: Option<String>, received_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
expires_at: Option<String>,
expired: bool,
} }
#[derive(Template)] #[derive(Template)]
@@ -72,6 +84,49 @@ struct SecretQuery {
field: String, field: String,
} }
fn parse_duration(s: &str) -> Option<chrono::Duration> {
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<String>) -> (Option<String>, 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<Vec<SecretData>> { async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
let secrets_api: Api<Secret> = Api::namespaced(state.client.clone(), &state.namespace); let secrets_api: Api<Secret> = Api::namespaced(state.client.clone(), &state.namespace);
let mut result = Vec::new(); let mut result = Vec::new();
@@ -97,7 +152,10 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
result.push(SecretData { result.push(SecretData {
name: secret_name.clone(), name: secret_name.clone(),
data: data_pairs, data: data_pairs,
source: SecretSource::Kubernetes,
received_at: None, received_at: None,
expires_at: None,
expired: false,
}); });
} }
Err(e) => { Err(e) => {
@@ -105,7 +163,10 @@ async fn read_secrets(state: &AppState) -> Result<Vec<SecretData>> {
result.push(SecretData { result.push(SecretData {
name: secret_name.clone(), name: secret_name.clone(),
data: vec![("error".to_string(), format!("Failed to read: {}", e))], data: vec![("error".to_string(), format!("Failed to read: {}", e))],
source: SecretSource::Kubernetes,
received_at: None, received_at: None,
expires_at: None,
expired: false,
}); });
} }
} }
@@ -141,10 +202,15 @@ async fn index_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse
.collect(); .collect();
data_pairs.sort_by(|a, b| a.0.cmp(&b.0)); 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 { all_secrets.push(SecretData {
name: webhook_secret.name.clone(), name: webhook_secret.name.clone(),
data: data_pairs, data: data_pairs,
source: SecretSource::Webhook,
received_at: Some(webhook_secret.received_at.clone()), received_at: Some(webhook_secret.received_at.clone()),
expires_at,
expired,
}); });
} }
} }

View File

@@ -21,12 +21,50 @@
padding: 15px; padding: 15px;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 4px; 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 { .secret-name {
font-weight: bold; font-weight: bold;
margin-bottom: 10px;
font-size: 16px; 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 { .data-item {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -114,8 +152,21 @@
</div> </div>
{% else %} {% else %}
{% for secret in secrets %} {% for secret in secrets %}
<div class="secret"> <div class="secret{% if secret.expired %} expired{% endif %}">
<div class="secret-header">
<div class="secret-name">{{ secret.name }}</div> <div class="secret-name">{{ secret.name }}</div>
{% match secret.source %}
{% when SecretSource::Kubernetes %}
<span class="source-badge k8s">☸ K8s</span>
{% when SecretSource::Webhook %}
<span class="source-badge webhook">⚡ Webhook</span>
{% endmatch %}
{% if secret.expired %}
{% if let Some(expires) = secret.expires_at %}
<span class="expired-badge">⚠ Expired at {{ expires }}</span>
{% endif %}
{% endif %}
</div>
{% for (key, value) in secret.data %} {% for (key, value) in secret.data %}
<div class="data-item"> <div class="data-item">
<span class="data-key">{{ key }}:</span> <span class="data-key">{{ key }}:</span>
@@ -132,7 +183,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% if let Some(received) = secret.received_at %} {% if let Some(received) = secret.received_at %}
<div class="received-at">Received: {{ received }}</div> <div class="received-at">Received: {{ received }}{% if let Some(expires) = secret.expires_at %}{% if !secret.expired %} · Expires: {{ expires }}{% endif %}{% endif %}</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}