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