use crate::api::{config::ApiConfig, middleware, routes, state::AppState};
use anyhow::Result;
use axum::{
Router, middleware as axum_middleware,
routing::{delete, get, post},
};
use std::sync::Arc;
use tower_http::compression::CompressionLayer;
use tracing::info;
pub struct ApiServer {
config: ApiConfig,
state: Arc<AppState>,
}
impl ApiServer {
pub fn new(config: ApiConfig) -> Result<Self> {
let state = Arc::new(AppState::new(config.clone())?);
Ok(Self { config, state })
}
fn build_router(&self) -> Router {
let api_routes = Router::new()
.route("/scan", post(routes::scans::create_scan))
.route("/scan/:id", get(routes::scans::get_scan_status))
.route("/scan/:id", delete(routes::scans::cancel_scan))
.route("/scan/:id/results", get(routes::scans::get_scan_results))
.route("/scan/:id/stream", get(routes::scans::websocket_handler))
.route(
"/certificates",
get(routes::certificates::list_certificates),
)
.route(
"/certificates/:fingerprint",
get(routes::certificates::get_certificate),
)
.route(
"/compliance/:framework",
get(routes::compliance::check_compliance),
)
.route("/policies", post(routes::policies::create_policy))
.route("/policies/:id", get(routes::policies::get_policy))
.route(
"/policies/:id/evaluate",
post(routes::policies::evaluate_policy),
)
.route("/history/:domain", get(routes::history::get_history))
.route("/stats", get(routes::stats::get_stats))
.route("/health", get(routes::health::health_check));
Router::new()
.nest("/api/v1", api_routes)
.route("/health", get(routes::health::health_check))
.merge(self.swagger_routes())
.layer(axum_middleware::from_fn_with_state(
self.state.clone(),
middleware::rate_limit,
))
.layer(axum_middleware::from_fn_with_state(
Arc::new(self.config.clone()),
middleware::authenticate,
))
.layer(middleware::cors_layer())
.layer(CompressionLayer::new())
.layer(middleware::logging_layer())
.with_state(self.state.clone())
}
fn swagger_routes(&self) -> Router<Arc<AppState>> {
if self.config.enable_swagger {
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
let openapi = crate::api::openapi::ApiDoc::openapi();
Router::new().merge(SwaggerUi::new("/api/docs").url("/api/docs/openapi.json", openapi))
} else {
Router::new()
}
}
pub async fn run(self) -> Result<()> {
let state = self.state.clone();
state.start_executor().await?;
let app = self.build_router();
let addr = format!("{}:{}", self.config.host, self.config.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
info!("CipherRun API server listening on {}", addr);
info!(
"OpenAPI documentation available at: http://{}/api/docs",
addr
);
info!("Health check endpoint: http://{}/health", addr);
axum::serve(listener, app).await?;
Ok(())
}
pub fn state(&self) -> Arc<AppState> {
self.state.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_creation() {
let config = ApiConfig::default();
let server = ApiServer::new(config);
assert!(server.is_ok());
}
#[tokio::test]
async fn test_router_build() {
let config = ApiConfig::default();
let server = ApiServer::new(config).expect("test assertion should succeed");
let _router = server.build_router();
}
}