feat: auto-generated per-repo webhook secrets with dashboard proxy
Some checks failed
CI / Format (push) Successful in 5s
CI / Clippy (push) Failing after 1m57s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
CI / Format (pull_request) Successful in 8s
CI / Clippy (pull_request) Failing after 1m53s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
Some checks failed
CI / Format (push) Successful in 5s
CI / Clippy (push) Failing after 1m57s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
CI / Format (pull_request) Successful in 8s
CI / Clippy (pull_request) Failing after 1m53s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
- Auto-generate webhook_secret on repository creation (UUID-based)
- Webhook routes use per-repo URLs: /webhook/{platform}/{repo_id}
- Verify signatures using per-repo secret (not global env var)
- Dashboard proxies webhooks to agent (agent not exposed publicly)
- Edit modal shows webhook URL + secret for user to copy into Gitea
- Add webhook-config API endpoint to retrieve per-repo secret
- Add Gitea option to edit dialog tracker type dropdown
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,31 @@ pub async fn trigger_repo_scan(repo_id: String) -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct WebhookConfigResponse {
|
||||
pub webhook_secret: Option<String>,
|
||||
pub tracker_type: String,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_webhook_config(repo_id: String) -> Result<WebhookConfigResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/repositories/{repo_id}/webhook-config",
|
||||
state.agent_api_url
|
||||
);
|
||||
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: WebhookConfigResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Check if a repository has any running scans
|
||||
#[server]
|
||||
pub async fn check_repo_scanning(repo_id: String) -> Result<bool, ServerFnError> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use axum::routing::get;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{middleware, Extension};
|
||||
use dioxus::prelude::*;
|
||||
use time::Duration;
|
||||
@@ -63,6 +63,8 @@ pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> {
|
||||
.route("/auth", get(auth_login))
|
||||
.route("/auth/callback", get(auth_callback))
|
||||
.route("/logout", get(logout))
|
||||
// Webhook proxy: forward to agent (no auth required)
|
||||
.route("/webhook/{platform}/{repo_id}", post(webhook_proxy))
|
||||
.serve_dioxus_application(ServeConfig::new(), app)
|
||||
.layer(Extension(PendingOAuthStore::default()))
|
||||
.layer(middleware::from_fn(require_auth))
|
||||
@@ -77,6 +79,53 @@ pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Forward incoming webhooks to the agent's webhook server.
|
||||
/// The dashboard acts as a public-facing proxy so the agent isn't exposed directly.
|
||||
async fn webhook_proxy(
|
||||
Extension(state): Extension<ServerState>,
|
||||
axum::extract::Path((platform, repo_id)): axum::extract::Path<(String, String)>,
|
||||
headers: axum::http::HeaderMap,
|
||||
body: axum::body::Bytes,
|
||||
) -> axum::http::StatusCode {
|
||||
// The agent_api_url typically looks like "http://agent:3001" or "http://localhost:3001"
|
||||
// Webhook routes are on the same server, so strip any trailing path
|
||||
let base = state.agent_api_url.trim_end_matches('/');
|
||||
// Remove /api/v1 suffix if present to get base URL
|
||||
let base = base
|
||||
.strip_suffix("/api/v1")
|
||||
.or_else(|| base.strip_suffix("/api"))
|
||||
.unwrap_or(base);
|
||||
let agent_url = format!("{base}/webhook/{platform}/{repo_id}");
|
||||
|
||||
// Forward all relevant headers
|
||||
let client = reqwest::Client::new();
|
||||
let mut req = client.post(&agent_url).body(body.to_vec());
|
||||
|
||||
for (name, value) in &headers {
|
||||
let name_str = name.as_str().to_lowercase();
|
||||
// Forward platform-specific headers
|
||||
if name_str.starts_with("x-gitea-")
|
||||
|| name_str.starts_with("x-github-")
|
||||
|| name_str.starts_with("x-hub-")
|
||||
|| name_str.starts_with("x-gitlab-")
|
||||
|| name_str == "content-type"
|
||||
{
|
||||
if let Ok(v) = value.to_str() {
|
||||
req = req.header(name.as_str(), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match req.send().await {
|
||||
Ok(resp) => axum::http::StatusCode::from_u16(resp.status().as_u16())
|
||||
.unwrap_or(axum::http::StatusCode::BAD_GATEWAY),
|
||||
Err(e) => {
|
||||
tracing::error!("Webhook proxy failed: {e}");
|
||||
axum::http::StatusCode::BAD_GATEWAY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Seed three default MCP server configs (Findings, SBOM, DAST) if they don't already exist.
|
||||
async fn seed_default_mcp_servers(db: &Database, mcp_endpoint_url: Option<&str>) {
|
||||
let endpoint = mcp_endpoint_url.unwrap_or("http://localhost:8090");
|
||||
|
||||
Reference in New Issue
Block a user