Skip to content

Commit 7511418

Browse files
scaffold crates/api-integration similar to crates/api-subscription with nango dep (#3703)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: yujonglee <yujonglee.dev@gmail.com>
1 parent b81b16f commit 7511418

File tree

10 files changed

+277
-0
lines changed

10 files changed

+277
-0
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ hypr-agc = { path = "crates/agc", package = "agc" }
2525
hypr-am = { path = "crates/am", package = "am" }
2626
hypr-am2 = { path = "crates/am2", package = "am2" }
2727
hypr-analytics = { path = "crates/analytics", package = "analytics" }
28+
hypr-api-integration = { path = "crates/api-integration", package = "api-integration" }
2829
hypr-api-subscription = { path = "crates/api-subscription", package = "api-subscription" }
2930
hypr-apple-note = { path = "crates/apple-note", package = "apple-note" }
3031
hypr-askama-utils = { path = "crates/askama-utils", package = "askama-utils" }

crates/api-integration/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "api-integration"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
hypr-nango = { workspace = true }
8+
hypr-supabase-auth = { workspace = true }
9+
10+
utoipa = { workspace = true }
11+
12+
axum = { workspace = true }
13+
reqwest = { workspace = true, features = ["json"] }
14+
sentry = { workspace = true }
15+
tokio = { workspace = true }
16+
tracing = { workspace = true }
17+
18+
serde = { workspace = true, features = ["derive"] }
19+
serde_json = { workspace = true }
20+
thiserror = { workspace = true }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use std::sync::Arc;
2+
3+
#[derive(Clone)]
4+
pub struct IntegrationConfig {
5+
pub nango_api_base: String,
6+
pub nango_api_key: String,
7+
pub auth: Option<Arc<hypr_supabase_auth::SupabaseAuth>>,
8+
}
9+
10+
impl IntegrationConfig {
11+
pub fn new(nango_api_base: impl Into<String>, nango_api_key: impl Into<String>) -> Self {
12+
Self {
13+
nango_api_base: nango_api_base.into(),
14+
nango_api_key: nango_api_key.into(),
15+
auth: None,
16+
}
17+
}
18+
19+
pub fn with_auth(mut self, auth: Arc<hypr_supabase_auth::SupabaseAuth>) -> Self {
20+
self.auth = Some(auth);
21+
self
22+
}
23+
}

crates/api-integration/src/env.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
use serde::Deserialize;
2+
3+
#[derive(Deserialize)]
4+
pub struct Env {
5+
pub nango_api_base: String,
6+
pub nango_api_key: String,
7+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use axum::{
2+
Json,
3+
http::StatusCode,
4+
response::{IntoResponse, Response},
5+
};
6+
use serde::Serialize;
7+
use thiserror::Error;
8+
9+
pub type Result<T> = std::result::Result<T, IntegrationError>;
10+
11+
#[derive(Debug, Serialize)]
12+
pub struct ErrorResponse {
13+
pub error: String,
14+
}
15+
16+
#[derive(Debug, Error)]
17+
pub enum IntegrationError {
18+
#[error("Authentication error: {0}")]
19+
Auth(String),
20+
21+
#[error("Nango error: {0}")]
22+
Nango(String),
23+
24+
#[error("Invalid request: {0}")]
25+
BadRequest(String),
26+
27+
#[error("Internal error: {0}")]
28+
Internal(String),
29+
}
30+
31+
impl From<hypr_supabase_auth::Error> for IntegrationError {
32+
fn from(err: hypr_supabase_auth::Error) -> Self {
33+
Self::Auth(err.to_string())
34+
}
35+
}
36+
37+
impl From<hypr_nango::Error> for IntegrationError {
38+
fn from(err: hypr_nango::Error) -> Self {
39+
Self::Nango(err.to_string())
40+
}
41+
}
42+
43+
impl IntoResponse for IntegrationError {
44+
fn into_response(self) -> Response {
45+
let (status, error_code) = match &self {
46+
Self::Auth(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
47+
Self::BadRequest(_) => (StatusCode::BAD_REQUEST, "bad_request"),
48+
Self::Nango(msg) => {
49+
tracing::error!(error = %msg, "nango_error");
50+
sentry::capture_message(msg, sentry::Level::Error);
51+
(StatusCode::INTERNAL_SERVER_ERROR, "nango_error")
52+
}
53+
Self::Internal(msg) => {
54+
tracing::error!(error = %msg, "internal_error");
55+
sentry::capture_message(msg, sentry::Level::Error);
56+
(StatusCode::INTERNAL_SERVER_ERROR, "internal_server_error")
57+
}
58+
};
59+
60+
let body = Json(ErrorResponse {
61+
error: error_code.to_string(),
62+
});
63+
64+
(status, body).into_response()
65+
}
66+
}

crates/api-integration/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
mod config;
2+
mod env;
3+
mod error;
4+
mod routes;
5+
mod state;
6+
7+
pub use config::IntegrationConfig;
8+
pub use env::Env;
9+
pub use error::{IntegrationError, Result};
10+
pub use routes::{openapi, router};
11+
pub use state::AppState;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use axum::{Json, extract::State, http::HeaderMap};
2+
use serde::Serialize;
3+
use utoipa::ToSchema;
4+
5+
use crate::error::{IntegrationError, Result};
6+
use crate::state::AppState;
7+
8+
#[derive(Debug, Serialize, ToSchema)]
9+
pub struct ConnectSessionResponse {
10+
pub token: String,
11+
pub expires_at: String,
12+
}
13+
14+
#[utoipa::path(
15+
post,
16+
path = "/connect-session",
17+
responses(
18+
(status = 200, description = "Connect session created", body = ConnectSessionResponse),
19+
(status = 401, description = "Unauthorized"),
20+
(status = 500, description = "Internal server error"),
21+
),
22+
tag = "integration",
23+
security(
24+
("bearer_auth" = [])
25+
)
26+
)]
27+
pub async fn create_connect_session(
28+
State(state): State<AppState>,
29+
headers: HeaderMap,
30+
) -> Result<Json<ConnectSessionResponse>> {
31+
let auth_token = extract_token(&headers)?;
32+
33+
let auth = state
34+
.config
35+
.auth
36+
.as_ref()
37+
.ok_or_else(|| IntegrationError::Auth("Auth not configured".to_string()))?;
38+
39+
let claims = auth
40+
.verify_token(auth_token)
41+
.await
42+
.map_err(|e| IntegrationError::Auth(e.to_string()))?;
43+
let user_id = claims.sub;
44+
45+
let req = hypr_nango::NangoConnectSessionRequest {
46+
end_user: hypr_nango::NangoConnectSessionRequestUser {
47+
id: user_id,
48+
display_name: None,
49+
email: None,
50+
},
51+
organization: None,
52+
allowed_integrations: vec![],
53+
integrations_config_defaults: None,
54+
};
55+
56+
let res = state.nango.create_connect_session(req).await?;
57+
58+
match res {
59+
hypr_nango::NangoConnectSessionResponse::Ok { token, expires_at } => {
60+
Ok(Json(ConnectSessionResponse { token, expires_at }))
61+
}
62+
hypr_nango::NangoConnectSessionResponse::Error { code } => {
63+
Err(IntegrationError::Nango(code))
64+
}
65+
}
66+
}
67+
68+
fn extract_token(headers: &HeaderMap) -> Result<&str> {
69+
let auth_header = headers
70+
.get("Authorization")
71+
.and_then(|h| h.to_str().ok())
72+
.ok_or_else(|| IntegrationError::Auth("Missing Authorization header".to_string()))?;
73+
74+
hypr_supabase_auth::SupabaseAuth::extract_token(auth_header)
75+
.ok_or_else(|| IntegrationError::Auth("Invalid Authorization header".to_string()))
76+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
mod connect;
2+
3+
use axum::{Router, routing::post};
4+
use utoipa::OpenApi;
5+
6+
use crate::state::AppState;
7+
8+
pub use connect::ConnectSessionResponse;
9+
10+
#[derive(OpenApi)]
11+
#[openapi(
12+
paths(
13+
connect::create_connect_session,
14+
),
15+
components(
16+
schemas(
17+
ConnectSessionResponse,
18+
)
19+
),
20+
tags(
21+
(name = "integration", description = "Integration management via Nango")
22+
)
23+
)]
24+
pub struct ApiDoc;
25+
26+
pub fn openapi() -> utoipa::openapi::OpenApi {
27+
ApiDoc::openapi()
28+
}
29+
30+
pub fn router(state: AppState) -> Router {
31+
Router::new()
32+
.route("/connect-session", post(connect::create_connect_session))
33+
.with_state(state)
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use hypr_nango::NangoClient;
2+
3+
use crate::config::IntegrationConfig;
4+
use crate::error::IntegrationError;
5+
6+
#[derive(Clone)]
7+
pub struct AppState {
8+
pub config: IntegrationConfig,
9+
pub nango: NangoClient,
10+
}
11+
12+
impl AppState {
13+
pub fn new(config: IntegrationConfig) -> Result<Self, IntegrationError> {
14+
let nango = hypr_nango::NangoClientBuilder::default()
15+
.api_base(&config.nango_api_base)
16+
.api_key(&config.nango_api_key)
17+
.build()
18+
.map_err(|e| IntegrationError::Nango(e.to_string()))?;
19+
20+
Ok(Self { config, nango })
21+
}
22+
}

0 commit comments

Comments
 (0)