use std::sync::Arc; use serde::{Deserialize, Serialize}; /// Webhook configuration for external integrations. #[derive(Debug, Clone)] pub struct WebhookConfig { pub webhook_url: Option, pub webhook_secret: Option, pub internal_auth_token: Option, pub web_instance_id: Option, pub web_instance_api_base_url: Option, client: reqwest::Client, } impl WebhookConfig { pub fn new( webhook_url: Option, webhook_secret: Option, internal_auth_token: Option, web_instance_id: Option, web_instance_api_base_url: Option, ) -> Self { WebhookConfig { webhook_url, webhook_secret, internal_auth_token, web_instance_id, web_instance_api_base_url, client: reqwest::Client::new(), } } pub fn is_enabled(&self) -> bool { self.webhook_url .as_deref() .is_some_and(|url| !url.trim().is_empty()) } pub fn has_internal_auth(&self) -> bool { self.internal_auth_token.is_some() } } // --- Request/Response types --- #[derive(Debug, Serialize)] pub struct ValidateTokenRequest { pub token: String, pub machine_id: String, pub hostname: String, pub version: String, pub os_type: Option, pub os_version: Option, pub os_distribution: Option, pub web_instance_id: Option, pub web_instance_api_base_url: Option, } #[derive(Debug, Deserialize)] pub struct ValidateTokenResponse { pub valid: bool, #[serde(default)] pub pre_approved: bool, #[serde(default)] pub binding_version: u64, pub network_config: Option, } #[derive(Debug, Serialize)] pub struct NodeConnectedRequest { pub machine_id: String, pub token: String, pub user_id: Option, pub hostname: String, pub version: String, pub os_type: Option, pub os_version: Option, pub os_distribution: Option, pub web_instance_id: Option, pub binding_version: Option, } #[derive(Debug, Serialize)] pub struct NodeDisconnectedRequest { pub machine_id: String, pub token: String, pub user_id: Option, pub web_instance_id: Option, pub binding_version: Option, } // --- Webhook client --- impl WebhookConfig { fn webhook_base_url(&self) -> anyhow::Result<&str> { self.webhook_url .as_deref() .map(str::trim) .filter(|url| !url.is_empty()) .ok_or_else(|| anyhow::anyhow!("webhook_url is not configured")) } fn webhook_endpoint(&self, path: &str) -> anyhow::Result { Ok(format!( "{}/{}", self.webhook_base_url()?.trim_end_matches('/'), path.trim_start_matches('/'), )) } /// Validate a token through the configured webhook endpoint. pub async fn validate_token( &self, req: &ValidateTokenRequest, ) -> anyhow::Result { let url = self.webhook_endpoint("validate-token")?; let resp = self .client .post(&url) .header("X-Internal-Auth", self.webhook_auth_secret()) .json(req) .send() .await?; if !resp.status().is_success() { anyhow::bail!("webhook validate-token returned status {}", resp.status()); } Ok(resp.json().await?) } /// Notify the webhook receiver that a node has connected. pub async fn notify_node_connected(&self, req: &NodeConnectedRequest) { if !self.is_enabled() { return; } let Ok(url) = self.webhook_endpoint("webhook/node-connected") else { tracing::warn!("skip node-connected webhook because webhook_url is not configured"); return; }; let _ = self .client .post(&url) .header("X-Internal-Auth", self.webhook_auth_secret()) .json(req) .send() .await; } /// Notify the webhook receiver that a node has disconnected. pub async fn notify_node_disconnected(&self, req: &NodeDisconnectedRequest) { if !self.is_enabled() { return; } let Ok(url) = self.webhook_endpoint("webhook/node-disconnected") else { tracing::warn!("skip node-disconnected webhook because webhook_url is not configured"); return; }; let _ = self .client .post(&url) .header("X-Internal-Auth", self.webhook_auth_secret()) .json(req) .send() .await; } fn webhook_auth_secret(&self) -> &str { self.webhook_secret .as_deref() .or(self.internal_auth_token.as_deref()) .unwrap_or("") } } pub type SharedWebhookConfig = Arc;