heldar_kernel/models/
webhooks.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::types::Json;
4use sqlx::FromRow;
5
6fn de_field_present<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error>
10where
11 D: serde::Deserializer<'de>,
12{
13 Ok(Some(Option::<String>::deserialize(deserializer)?))
14}
15
16pub fn mask_webhook_url(url: &str) -> Option<String> {
20 let url = url.trim();
21 if url.is_empty() {
22 return None;
23 }
24 match url.split_once("://") {
25 Some((scheme, rest)) => {
26 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
27 let authority = &rest[..authority_end];
28 if authority_end < rest.len() {
29 Some(format!("{scheme}://{authority}/…"))
30 } else {
31 Some(format!("{scheme}://{authority}"))
32 }
33 }
34 None => Some("…".to_string()),
35 }
36}
37
38#[derive(Debug, Clone, FromRow)]
43pub struct WebhookSubscription {
44 pub id: String,
45 pub name: String,
46 pub url: String,
47 pub event_types: Json<Vec<String>>,
48 pub min_severity: String,
49 pub secret: Option<String>,
50 pub enabled: bool,
51 pub cursor_at: Option<DateTime<Utc>>,
52 pub created_at: DateTime<Utc>,
53 pub updated_at: DateTime<Utc>,
54}
55
56#[derive(Debug, Clone, Serialize)]
58pub struct WebhookSubscriptionView {
59 pub id: String,
60 pub name: String,
61 pub url: String,
62 pub event_types: Vec<String>,
63 pub min_severity: String,
64 pub has_secret: bool,
66 pub enabled: bool,
67 pub cursor_at: Option<DateTime<Utc>>,
68 pub created_at: DateTime<Utc>,
69 pub updated_at: DateTime<Utc>,
70}
71
72impl From<WebhookSubscription> for WebhookSubscriptionView {
73 fn from(s: WebhookSubscription) -> Self {
74 WebhookSubscriptionView {
75 id: s.id,
76 name: s.name,
77 url: s.url,
78 event_types: s.event_types.0,
79 min_severity: s.min_severity,
80 has_secret: s.secret.as_deref().map(|v| !v.is_empty()).unwrap_or(false),
81 enabled: s.enabled,
82 cursor_at: s.cursor_at,
83 created_at: s.created_at,
84 updated_at: s.updated_at,
85 }
86 }
87}
88
89#[derive(Debug, Deserialize)]
90pub struct WebhookSubscriptionCreate {
91 pub name: String,
92 pub url: String,
93 pub event_types: Option<Vec<String>>,
95 pub min_severity: Option<String>,
97 pub secret: Option<String>,
99 pub enabled: Option<bool>,
100}
101
102#[derive(Debug, Deserialize, Default)]
106pub struct WebhookSubscriptionUpdate {
107 pub name: Option<String>,
108 pub url: Option<String>,
109 pub event_types: Option<Vec<String>>,
110 pub min_severity: Option<String>,
111 #[serde(default, deserialize_with = "de_field_present")]
112 pub secret: Option<Option<String>>,
113 pub enabled: Option<bool>,
114}
115
116#[derive(Debug, Clone, Serialize, FromRow)]
118pub struct WebhookDelivery {
119 pub id: String,
120 pub subscription_id: String,
121 pub event_id: Option<String>,
122 pub event_type: Option<String>,
123 pub status: String,
124 pub attempts: i64,
125 pub response_code: Option<i64>,
126 pub error: Option<String>,
127 pub created_at: DateTime<Utc>,
128 pub delivered_at: Option<DateTime<Utc>>,
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn mask_webhook_url_hides_path_and_token() {
137 assert_eq!(
139 mask_webhook_url("https://hooks.slack.com/services/T000/B000/XXXXSECRET"),
140 Some("https://hooks.slack.com/…".to_string())
141 );
142 assert_eq!(
143 mask_webhook_url("https://example.com:8443/alert?token=abc"),
144 Some("https://example.com:8443/…".to_string())
145 );
146 assert_eq!(
148 mask_webhook_url("https://example.com"),
149 Some("https://example.com".to_string())
150 );
151 assert_eq!(mask_webhook_url(" "), None);
153 assert_eq!(mask_webhook_url("not-a-url"), Some("…".to_string()));
154 }
155
156 #[test]
157 fn webhook_update_secret_is_three_state() {
158 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"enabled": true}"#).unwrap();
160 assert!(u.secret.is_none());
161 assert_eq!(u.enabled, Some(true));
162 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"secret": null}"#).unwrap();
164 assert_eq!(u.secret, Some(None));
165 let u: WebhookSubscriptionUpdate = serde_json::from_str(r#"{"secret": "s3cr3t"}"#).unwrap();
167 assert_eq!(u.secret, Some(Some("s3cr3t".to_string())));
168 }
169}