raisfast 0.2.23

The last backend you'll ever need. Rust-powered headless CMS with built-in blog, ecommerce, wallet, payment and 4 plugin engines.
use crate::db::Pool;
use crate::errors::app_error::AppResult;
use crate::types::snowflake_id::SnowflakeId;
use crate::webhook::model;

pub struct WebhookService {
    pool: Pool,
}

impl WebhookService {
    pub fn new(pool: Pool) -> Self {
        Self { pool }
    }

    pub async fn create(
        &self,
        tenant_id: Option<&str>,
        url: String,
        events: Vec<String>,
        description: Option<String>,
        enabled: bool,
        custom_secret: Option<String>,
    ) -> AppResult<model::WebhookSubscription> {
        let (id, now) = (
            crate::utils::id::new_snowflake_id(),
            crate::utils::tz::now_utc(),
        );
        let secret = custom_secret.unwrap_or_else(Self::generate_secret);
        let sub = model::WebhookSubscription {
            id,
            tenant_id: tenant_id.map(|t| t.to_string()),
            url,
            secret,
            events: serde_json::to_string(&events).unwrap_or_default(),
            enabled,
            description,
            created_at: now,
            updated_at: now,
        };
        model::insert(&self.pool, &sub).await?;
        let inserted = model::find_by_id(&self.pool, sub.id).await?;
        Ok(inserted)
    }

    pub async fn list(
        &self,
        tenant_id: Option<&str>,
        page: i64,
        page_size: i64,
    ) -> AppResult<(Vec<model::WebhookSubscription>, i64)> {
        model::find_paginated(&self.pool, tenant_id, page, page_size).await
    }

    pub async fn get(&self, id: SnowflakeId) -> AppResult<model::WebhookSubscription> {
        model::find_by_id(&self.pool, id).await
    }

    #[allow(clippy::too_many_arguments)]
    pub async fn update(
        &self,
        id: SnowflakeId,
        url: Option<String>,
        events: Option<Vec<String>>,
        description: Option<String>,
        enabled: Option<bool>,
    ) -> AppResult<model::WebhookSubscription> {
        let mut sub = model::find_by_id(&self.pool, id).await?;
        let (_, now) = (
            crate::utils::id::new_snowflake_id(),
            crate::utils::tz::now_utc(),
        );
        if let Some(u) = url {
            sub.url = u;
        }
        if let Some(e) = events {
            sub.events = serde_json::to_string(&e).unwrap_or_default();
        }
        if description.is_some() {
            sub.description = description;
        }
        if let Some(en) = enabled {
            sub.enabled = en;
        }
        sub.updated_at = now;
        model::update(&self.pool, &sub).await?;
        Ok(sub)
    }

    pub async fn delete(&self, id: SnowflakeId) -> AppResult<()> {
        model::delete_by_id(&self.pool, id).await
    }

    pub async fn find_enabled(
        &self,
        tenant_id: Option<&str>,
    ) -> AppResult<Vec<model::WebhookSubscription>> {
        model::find_enabled_by_tenant(&self.pool, tenant_id).await
    }

    fn generate_secret() -> String {
        crate::utils::id::random_hex(32)
    }

    pub fn sign_payload(secret: &str, body: &[u8]) -> String {
        use hmac::{Hmac, Mac};
        type HmacSha256 = Hmac<sha2::Sha256>;
        let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
            .expect("HMAC-SHA256 accepts keys of any size per RFC 2104");
        mac.update(body);
        hex::encode(mac.finalize().into_bytes())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generate_secret_length() {
        let secret = WebhookService::generate_secret();
        assert_eq!(secret.len(), 64);
    }
}