Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ objc2-event-kit = "0.3"
objc2-foundation = "0.3"
objc2-user-notifications = "0.3"

hmac = "0.12"
sha2 = "0.10"

tokenizers = "0.21.4"
whichlang = "0.1"

Expand Down
3 changes: 3 additions & 0 deletions crates/api-integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ sentry = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

hmac = { workspace = true }
sha2 = { workspace = true }

serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
7 changes: 7 additions & 0 deletions crates/api-integration/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::sync::Arc;
pub struct IntegrationConfig {
pub nango_api_base: String,
pub nango_api_key: String,
pub nango_webhook_secret: String,
pub auth: Option<Arc<hypr_supabase_auth::SupabaseAuth>>,
}

Expand All @@ -12,10 +13,16 @@ impl IntegrationConfig {
Self {
nango_api_base: nango_api_base.into(),
nango_api_key: nango_api_key.into(),
nango_webhook_secret: String::new(),
auth: None,
}
}

pub fn with_webhook_secret(mut self, secret: impl Into<String>) -> Self {
self.nango_webhook_secret = secret.into();
self
}

pub fn with_auth(mut self, auth: Arc<hypr_supabase_auth::SupabaseAuth>) -> Self {
self.auth = Some(auth);
self
Expand Down
1 change: 1 addition & 0 deletions crates/api-integration/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ use serde::Deserialize;
pub struct Env {
pub nango_api_base: String,
pub nango_api_key: String,
pub nango_webhook_secret: String,
}
2 changes: 1 addition & 1 deletion crates/api-integration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ mod state;
pub use config::IntegrationConfig;
pub use env::Env;
pub use error::{IntegrationError, Result};
pub use routes::{openapi, router};
pub use routes::{WebhookResponse, openapi, router};
pub use state::AppState;
5 changes: 5 additions & 0 deletions crates/api-integration/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
mod connect;
mod webhook;

use axum::{Router, routing::post};
use utoipa::OpenApi;

use crate::state::AppState;

pub use connect::ConnectSessionResponse;
pub use webhook::WebhookResponse;

#[derive(OpenApi)]
#[openapi(
paths(
connect::create_connect_session,
webhook::nango_webhook,
),
components(
schemas(
ConnectSessionResponse,
WebhookResponse,
)
),
tags(
Expand All @@ -30,5 +34,6 @@ pub fn openapi() -> utoipa::openapi::OpenApi {
pub fn router(state: AppState) -> Router {
Router::new()
.route("/connect-session", post(connect::create_connect_session))
.route("/webhook", post(webhook::nango_webhook))
.with_state(state)
}
68 changes: 68 additions & 0 deletions crates/api-integration/src/routes/webhook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use axum::{Json, extract::State, http::HeaderMap};
use hmac::{Hmac, Mac};
use serde::Serialize;
use sha2::Sha256;
use utoipa::ToSchema;

use crate::error::{IntegrationError, Result};
use crate::state::AppState;

type HmacSha256 = Hmac<Sha256>;

#[derive(Debug, Serialize, ToSchema)]
pub struct WebhookResponse {
pub status: String,
}

#[utoipa::path(
post,
path = "/webhook",
responses(
(status = 200, description = "Webhook processed", body = WebhookResponse),
(status = 401, description = "Invalid signature"),
(status = 400, description = "Bad request"),
),
tag = "integration",
)]
pub async fn nango_webhook(
State(state): State<AppState>,
headers: HeaderMap,
body: String,
) -> Result<Json<WebhookResponse>> {
verify_signature(&state.config.nango_webhook_secret, &body, &headers)?;

let payload: hypr_nango::ConnectWebhook =

Check failure on line 34 in crates/api-integration/src/routes/webhook.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (macos, depot-macos-14)

cannot find type `ConnectWebhook` in crate `hypr_nango`

Check failure on line 34 in crates/api-integration/src/routes/webhook.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

cannot find type `ConnectWebhook` in crate `hypr_nango`
serde_json::from_str(&body).map_err(|e| IntegrationError::BadRequest(e.to_string()))?;

tracing::info!(
webhook_type = %payload.r#type,
operation = %payload.operation,
connection_id = %payload.connection_id,
end_user_id = %payload.end_user.end_user_id,
"nango webhook received"
);

Ok(Json(WebhookResponse {
status: "ok".to_string(),
}))
}

fn verify_signature(secret: &str, body: &str, headers: &HeaderMap) -> Result<()> {
let signature = headers
.get("x-nango-hmac-sha256")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| IntegrationError::Auth("Missing X-Nango-Hmac-Sha256 header".to_string()))?;

let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|e| IntegrationError::Internal(format!("HMAC init error: {}", e)))?;
mac.update(body.as_bytes());
let expected = format!("{:x}", mac.finalize().into_bytes());

if expected != signature {
return Err(IntegrationError::Auth(
"Invalid webhook signature".to_string(),
));
}

Ok(())
}
Loading