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