karbon_framework/http/
app.rs1use 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#[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
23pub struct App {
25 config: Config,
26 router: Option<Router<AppState>>,
27}
28
29impl App {
30 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 pub fn with_config(config: Config) -> Self {
42 Self {
43 config,
44 router: None,
45 }
46 }
47
48 pub fn router(mut self, router: Router<AppState>) -> Self {
50 self.router = Some(router);
51 self
52 }
53
54 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 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 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 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 if cfg!(debug_assertions) {
131 tracing::info!("Debug profiler enabled");
132 router = router.layer(middleware::from_fn(crate::logger::profiler_middleware));
133 }
134
135 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
171async 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}