1use crate::middleware::{comprehensive_security_middleware, SecurityState};
4use crate::routes::{
5 delete_config, get_config, get_history, health_check, list_configs, rollback_config,
6 set_config, ApiState,
7};
8use axum::{
9 middleware,
10 routing::{delete, get, post},
11 Router,
12};
13use llm_config_core::ConfigManager;
14use std::net::SocketAddr;
15use std::sync::Arc;
16use tower_http::cors::{Any, CorsLayer};
17use tower_http::trace::TraceLayer;
18
19#[derive(Debug, Clone)]
21pub struct ServerConfig {
22 pub host: String,
23 pub port: u16,
24 pub enable_cors: bool,
25 pub enable_security: bool,
26}
27
28impl Default for ServerConfig {
29 fn default() -> Self {
30 Self {
31 host: "127.0.0.1".to_string(),
32 port: 8080,
33 enable_cors: true,
34 enable_security: true,
35 }
36 }
37}
38
39pub fn create_router(manager: Arc<ConfigManager>, security_state: SecurityState) -> Router {
41 let api_state = ApiState { manager };
42
43 let api_routes = Router::new()
45 .route("/configs/:namespace/:key", get(get_config))
47 .route("/configs/:namespace/:key", post(set_config))
48 .route("/configs/:namespace/:key", delete(delete_config))
49 .route("/configs/:namespace", get(list_configs))
50 .route("/configs/:namespace/:key/history", get(get_history))
52 .route(
53 "/configs/:namespace/:key/rollback/:version",
54 post(rollback_config),
55 )
56 .layer(middleware::from_fn_with_state(
57 security_state.clone(),
58 comprehensive_security_middleware,
59 ))
60 .with_state(api_state);
61
62 Router::new()
64 .route("/health", get(health_check))
65 .nest("/api/v1", api_routes)
66}
67
68pub async fn serve(
70 manager: Arc<ConfigManager>,
71 config: ServerConfig,
72) -> anyhow::Result<()> {
73 let security_state = if config.enable_security {
75 SecurityState::new()
76 } else {
77 SecurityState::new() };
79
80 let app = create_router(manager, security_state);
81
82 let app = app
84 .layer(TraceLayer::new_for_http())
85 .layer(if config.enable_cors {
86 CorsLayer::new()
87 .allow_origin(Any)
88 .allow_methods(Any)
89 .allow_headers(Any)
90 } else {
91 CorsLayer::permissive()
92 });
93
94 let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
96 tracing::info!(
97 "Starting LLM Config API server on {} (security: {})",
98 addr,
99 config.enable_security
100 );
101
102 let listener = tokio::net::TcpListener::bind(addr).await?;
104
105 axum::serve(
107 listener,
108 app.into_make_service_with_connect_info::<SocketAddr>(),
109 )
110 .with_graceful_shutdown(shutdown_signal())
111 .await?;
112
113 Ok(())
114}
115
116async fn shutdown_signal() {
118 let ctrl_c = async {
119 tokio::signal::ctrl_c()
120 .await
121 .expect("Failed to install Ctrl+C handler");
122 };
123
124 #[cfg(unix)]
125 let terminate = async {
126 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
127 .expect("Failed to install signal handler")
128 .recv()
129 .await;
130 };
131
132 #[cfg(not(unix))]
133 let terminate = std::future::pending::<()>();
134
135 tokio::select! {
136 _ = ctrl_c => {
137 tracing::info!("Received Ctrl+C, shutting down gracefully");
138 },
139 _ = terminate => {
140 tracing::info!("Received SIGTERM, shutting down gracefully");
141 },
142 }
143}
144
145