use crate::AppService; use crate::error::AppError; use chrono::Utc; use models::repos::repo_webhook; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; #[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] pub struct WebhookEvent { #[serde(default)] pub push: bool, #[serde(default)] pub tag_push: bool, #[serde(default)] pub pull_request: bool, #[serde(default)] pub issue_comment: bool, #[serde(default)] pub release: bool, } impl Default for WebhookEvent { fn default() -> Self { Self { push: true, tag_push: false, pull_request: false, issue_comment: false, release: false, } } } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreateWebhookParams { pub url: String, #[serde(default)] pub content_type: Option, #[serde(default)] pub secret: Option, #[serde(default)] pub insecure_ssl: Option, #[serde(default)] pub events: WebhookEvent, #[serde(default = "default_active")] pub active: bool, } fn default_active() -> bool { true } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct UpdateWebhookParams { pub url: Option, pub content_type: Option, pub secret: Option, pub insecure_ssl: Option, pub events: Option, pub active: Option, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct WebhookResponse { pub id: i64, pub repo_uuid: String, pub url: String, pub content_type: String, pub secret: Option, pub events: WebhookEvent, pub active: bool, pub created_at: chrono::DateTime, pub last_delivered_at: Option>, pub touch_count: i64, } impl From for WebhookResponse { fn from(m: repo_webhook::Model) -> Self { let events: WebhookEvent = serde_json::from_value(m.event.clone()).unwrap_or_default(); let url = m.url.unwrap_or_default(); let (content_type, secret, active) = serde_json::from_value::(m.event.clone()) .ok() .map(|v| { ( v.get("content_type") .and_then(|v| v.as_str()) .unwrap_or("json") .to_string(), v.get("secret").and_then(|v| v.as_str()).map(String::from), v.get("active").and_then(|v| v.as_bool()).unwrap_or(true), ) }) .unwrap_or(("json".to_string(), None, true)); WebhookResponse { id: m.id, repo_uuid: m.repo.to_string(), url, content_type, secret, events, active, created_at: m.created_at, last_delivered_at: m.last_delivered_at, touch_count: m.touch_count, } } } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct WebhookListResponse { pub webhooks: Vec, pub total: usize, } impl AppService { pub async fn git_webhook_list( &self, namespace: String, repo_name: String, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let webhooks = repo_webhook::Entity::find() .filter(repo_webhook::Column::Repo.eq(repo.id)) .order_by_asc(repo_webhook::Column::Id) .all(&self.db) .await?; let total = webhooks.len(); let webhooks = webhooks.into_iter().map(WebhookResponse::from).collect(); Ok(WebhookListResponse { webhooks, total }) } pub async fn git_webhook_create( &self, namespace: String, repo_name: String, params: CreateWebhookParams, ctx: &Session, ) -> Result { let repo = self .utils_find_repo(namespace.clone(), repo_name.clone(), ctx) .await?; let _ = self .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx) .await?; let event_json = serde_json::json!({ "push": params.events.push, "tag_push": params.events.tag_push, "pull_request": params.events.pull_request, "issue_comment": params.events.issue_comment, "release": params.events.release, "content_type": params.content_type.unwrap_or_else(|| "json".to_string()), "secret": params.secret, "active": params.active, }); let model = repo_webhook::ActiveModel { repo: Set(repo.id), event: Set(event_json), url: Set(Some(params.url)), access_key: Set(None), secret_key: Set(params.secret), created_at: Set(Utc::now()), last_delivered_at: Set(None), touch_count: Set(0), ..Default::default() } .insert(&self.db) .await?; Ok(WebhookResponse::from(model)) } pub async fn git_webhook_get( &self, namespace: String, repo_name: String, webhook_id: i64, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let webhook = repo_webhook::Entity::find_by_id(webhook_id) .one(&self.db) .await? .ok_or(AppError::NotFound("Webhook not found".to_string()))?; if webhook.repo != repo.id { return Err(AppError::NotFound("Webhook not found".to_string())); } Ok(WebhookResponse::from(webhook)) } pub async fn git_webhook_update( &self, namespace: String, repo_name: String, webhook_id: i64, params: UpdateWebhookParams, ctx: &Session, ) -> Result { let repo = self .utils_find_repo(namespace.clone(), repo_name.clone(), ctx) .await?; let _ = self .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx) .await?; let webhook = repo_webhook::Entity::find_by_id(webhook_id) .one(&self.db) .await? .ok_or(AppError::NotFound("Webhook not found".to_string()))?; if webhook.repo != repo.id { return Err(AppError::NotFound("Webhook not found".to_string())); } let mut active: repo_webhook::ActiveModel = webhook.clone().into(); if let Some(url) = params.url { active.url = Set(Some(url)); } let secret_val = params.secret.clone(); if let Some(secret) = params.secret { active.secret_key = Set(Some(secret)); } if params.events.is_some() || params.content_type.is_some() || params.active.is_some() { let existing: serde_json::Value = webhook.event.clone(); let events = params .events .unwrap_or_else(|| serde_json::from_value(existing.clone()).unwrap_or_default()); let content_type = params .content_type .or_else(|| { existing .get("content_type") .and_then(|v| v.as_str()) .map(String::from) }) .unwrap_or_else(|| "json".to_string()); let active_val = params .active .or_else(|| existing.get("active").and_then(|v| v.as_bool())) .unwrap_or(true); let secret_in_event = existing .get("secret") .and_then(|v| v.as_str()) .map(String::from); let final_secret = secret_val.clone().or(secret_in_event); active.event = Set(serde_json::json!({ "push": events.push, "tag_push": events.tag_push, "pull_request": events.pull_request, "issue_comment": events.issue_comment, "release": events.release, "content_type": content_type, "secret": final_secret, "active": active_val, })); } let updated = active.update(&self.db).await?; Ok(WebhookResponse::from(updated)) } pub async fn git_webhook_delete( &self, namespace: String, repo_name: String, webhook_id: i64, ctx: &Session, ) -> Result<(), AppError> { let repo = self .utils_find_repo(namespace.clone(), repo_name.clone(), ctx) .await?; let _ = self .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx) .await?; let webhook = repo_webhook::Entity::find_by_id(webhook_id) .one(&self.db) .await? .ok_or(AppError::NotFound("Webhook not found".to_string()))?; if webhook.repo != repo.id { return Err(AppError::NotFound("Webhook not found".to_string())); } repo_webhook::Entity::delete_by_id(webhook_id) .exec(&self.db) .await?; Ok(()) } }