Skip to main content

karbon_framework/http/
app.rs

1use axum::{middleware, Router};
2use std::net::SocketAddr;
3use tokio::net::TcpListener;
4use tower_http::compression::CompressionLayer;
5use tower_http::cors::{Any, CorsLayer};
6use tower_http::trace::TraceLayer;
7use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
8#[cfg(feature = "studio")]
9use axum::Extension;
10
11use crate::config::Config;
12use crate::db::Database;
13use crate::mail::Mailer;
14use crate::security::RoleHierarchy;
15
16/// Application state shared across all handlers
17#[derive(Clone)]
18pub struct AppState {
19    pub db: Database,
20    pub config: Config,
21    pub mailer: Option<Mailer>,
22    pub role_hierarchy: RoleHierarchy,
23}
24
25/// Main application builder
26pub struct App {
27    config: Config,
28    router: Option<Router<AppState>>,
29}
30
31impl App {
32    /// Create a new App from environment config
33    pub fn new() -> Self {
34        dotenvy::dotenv().ok();
35        let config = Config::from_env();
36        Self {
37            config,
38            router: None,
39        }
40    }
41
42    /// Create a new App with a specific config
43    pub fn with_config(config: Config) -> Self {
44        Self {
45            config,
46            router: None,
47        }
48    }
49
50    /// Set the application router
51    pub fn router(mut self, router: Router<AppState>) -> Self {
52        self.router = Some(router);
53        self
54    }
55
56    /// Initialize tracing/logging
57    fn init_tracing(&self) {
58        let filter = EnvFilter::try_from_default_env()
59            .unwrap_or_else(|_| EnvFilter::new(&self.config.log_level));
60
61        tracing_subscriber::registry()
62            .with(filter)
63            .with(tracing_subscriber::fmt::layer())
64            .init();
65    }
66
67    /// Build CORS layer from config
68    fn cors_layer(&self) -> CorsLayer {
69        let cors = CorsLayer::new()
70            .allow_methods(Any)
71            .allow_headers(Any);
72
73        if self.config.cors_origins.contains(&"*".to_string()) {
74            cors.allow_origin(Any)
75        } else if self.config.cors_origins.is_empty() {
76            tracing::warn!("CORS_ORIGINS is empty — no cross-origin requests will be allowed");
77            cors
78        } else {
79            let origins: Vec<_> = self
80                .config
81                .cors_origins
82                .iter()
83                .filter_map(|o| o.parse().ok())
84                .collect();
85            cors.allow_origin(origins)
86        }
87    }
88
89    /// Start the server with graceful shutdown support
90    pub async fn serve(self) -> anyhow::Result<()> {
91        self.init_tracing();
92
93        tracing::info!(
94            "Connecting to database at {}:{}",
95            self.config.db_host,
96            self.config.db_port
97        );
98
99        let db = Database::connect(&self.config).await?;
100        tracing::info!("Database connected");
101
102        // Initialize mailer if SMTP is configured
103        let mailer = if !self.config.smtp_host.is_empty() && !self.config.smtp_user.is_empty() {
104            match Mailer::new(&self.config) {
105                Ok(m) => {
106                    tracing::info!("Mailer initialized ({})", self.config.smtp_host);
107                    Some(m)
108                }
109                Err(e) => {
110                    tracing::warn!("Mailer initialization failed: {} — emails will be disabled", e);
111                    None
112                }
113            }
114        } else {
115            tracing::info!("Mailer not configured — emails disabled");
116            None
117        };
118
119        let state = AppState {
120            db,
121            config: self.config.clone(),
122            mailer,
123            role_hierarchy: crate::security::default_hierarchy(),
124        };
125
126        let cors = self.cors_layer();
127        let mut router = self
128            .router
129            .unwrap_or_else(Router::new);
130
131        // Profiler activé uniquement en mode debug
132        if cfg!(debug_assertions) {
133            tracing::info!("Debug profiler enabled");
134            router = router.layer(middleware::from_fn(crate::logger::profiler_middleware));
135        }
136
137        // Studio (dev dashboard)
138        #[cfg(feature = "studio")]
139        let studio_enabled = cfg!(debug_assertions) || std::env::var("KARBON_STUDIO").is_ok();
140        #[cfg(feature = "studio")]
141        let studio_collector = if studio_enabled {
142            let (studio_router, collector, token) = crate::studio::build();
143            // Studio router already has its own state, merge at Router<()> level later
144            tracing::info!(
145                "\x1b[38;5;141m⚡ Studio\x1b[0m → http://{}:{}/_studio?token={}",
146                "localhost",
147                self.config.port,
148                token
149            );
150            Some((studio_router, collector))
151        } else {
152            None
153        };
154
155        // Frontend reverse proxy (enabled via KARBON_FRONTEND_URL env var)
156        if let Ok(frontend_url) = std::env::var("KARBON_FRONTEND_URL") {
157            tracing::info!("Frontend proxy enabled → {}", frontend_url);
158            let proxy = super::FrontendProxy::new(&frontend_url);
159            router = router.fallback(move |req| proxy.clone().handle(req));
160        }
161
162        #[allow(unused_mut)]
163        let mut router = router
164            .layer(middleware::from_fn(super::middleware::request_id))
165            .layer(CompressionLayer::new())
166            .layer(cors)
167            .layer(TraceLayer::new_for_http())
168            .with_state(state);
169
170        #[cfg(feature = "studio")]
171        if let Some((studio_router, collector)) = studio_collector {
172            router = router
173                .nest("/_studio", studio_router)
174                .layer(middleware::from_fn(crate::studio::middleware::studio_middleware))
175                .layer(Extension(collector));
176        }
177
178        let addr = SocketAddr::from(([0, 0, 0, 0], self.config.port));
179        tracing::info!("Server starting on http://{}", addr);
180
181        let listener = TcpListener::bind(addr).await?;
182        axum::serve(
183            listener,
184            router.into_make_service_with_connect_info::<SocketAddr>(),
185        )
186        .with_graceful_shutdown(shutdown_signal())
187        .await?;
188
189        tracing::info!("Server shut down gracefully");
190        Ok(())
191    }
192}
193
194impl Default for App {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200/// Wait for SIGINT (Ctrl+C) or SIGTERM to gracefully shut down
201async fn shutdown_signal() {
202    let ctrl_c = async {
203        tokio::signal::ctrl_c()
204            .await
205            .expect("Failed to install Ctrl+C handler");
206    };
207
208    #[cfg(unix)]
209    let terminate = async {
210        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
211            .expect("Failed to install SIGTERM handler")
212            .recv()
213            .await;
214    };
215
216    #[cfg(not(unix))]
217    let terminate = std::future::pending::<()>();
218
219    tokio::select! {
220        _ = ctrl_c => tracing::info!("Received Ctrl+C, shutting down..."),
221        _ = terminate => tracing::info!("Received SIGTERM, shutting down..."),
222    }
223}