use super::shutdown::{ShutdownCoordinator, coordinated_shutdown, shutdown_signal};
use crate::errors::handlers::not_found;
use crate::http::security::security_headers;
use axum::{Router, middleware};
use core_config::server::ServerConfig;
use std::io;
use std::time::Duration;
use tower_http::compression::CompressionLayer;
use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
use tracing::{Level, info};
use utoipa::OpenApi;
pub async fn create_app(router: Router, server_config: &ServerConfig) -> io::Result<()> {
let listener = tokio::net::TcpListener::bind(server_config.addr()).await?;
info!("Server starting on {}", listener.local_addr()?);
axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.await
.inspect_err(|e| {
tracing::error!("Server encountered an error: {e:?}");
})?;
Ok(())
}
pub async fn create_router<T>(apis: Router) -> io::Result<Router>
where
T: OpenApi + 'static,
{
use utoipa_rapidoc::RapiDoc;
use utoipa_redoc::{Redoc, Servable as RedocServable};
use utoipa_scalar::{Scalar, Servable as ScalarServable};
use utoipa_swagger_ui::SwaggerUi;
use axum::http::{HeaderName, Method};
use tower_http::cors::AllowOrigin;
let origins_str = std::env::var("CORS_ALLOWED_ORIGIN")
.map_err(|_| io::Error::new(
io::ErrorKind::InvalidInput,
"CORS_ALLOWED_ORIGIN environment variable is required. Example: CORS_ALLOWED_ORIGIN=http://localhost:3000,https://example.com"
))?;
let allowed_origins: Vec<axum::http::HeaderValue> = origins_str
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.parse::<axum::http::HeaderValue>())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("Invalid CORS_ALLOWED_ORIGIN value: {e}"),
)
})?;
if allowed_origins.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"CORS_ALLOWED_ORIGIN cannot be empty",
));
}
info!("CORS configured with allowed origins: {origins_str}");
let cors_layer = tower_http::cors::CorsLayer::new()
.allow_origin(AllowOrigin::list(allowed_origins))
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::PATCH,
Method::OPTIONS,
])
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
axum::http::header::ACCEPT,
axum::http::header::COOKIE,
HeaderName::from_static("x-csrf-token"),
])
.allow_credentials(true)
.max_age(Duration::from_secs(3600));
let router = Router::new()
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", T::openapi()))
.merge(Redoc::with_url("/redoc", T::openapi()))
.merge(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc"))
.merge(Scalar::with_url("/scalar", T::openapi()))
.nest("/api", apis)
.fallback(not_found)
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
.on_response(DefaultOnResponse::new().level(Level::INFO)),
)
.layer(middleware::from_fn(security_headers))
.layer(cors_layer)
.layer(CompressionLayer::new());
Ok(router)
}
pub async fn create_production_app<F>(
router: Router,
server_config: &ServerConfig,
shutdown_timeout: Duration,
cleanup: F,
) -> io::Result<()>
where
F: std::future::Future<Output = ()> + Send + 'static,
{
let (coordinator, _rx) = ShutdownCoordinator::new();
let shutdown_handle = coordinator.clone();
let listener = tokio::net::TcpListener::bind(server_config.addr()).await?;
info!("Server starting on {}", listener.local_addr()?);
let cleanup_handle = tokio::spawn(async move {
shutdown_handle.wait_for_signal().await;
info!("Starting cleanup tasks (timeout: {:?})", shutdown_timeout);
let cleanup_result = tokio::time::timeout(shutdown_timeout, cleanup).await;
match cleanup_result {
Ok(_) => info!("Cleanup completed successfully"),
Err(_) => {
tracing::warn!(
"Cleanup exceeded timeout of {:?}, forcing shutdown",
shutdown_timeout
);
}
}
});
let serve_result = axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(coordinated_shutdown(coordinator))
.await
.inspect_err(|e| {
tracing::error!("Server encountered an error: {e:?}");
});
cleanup_handle.await.ok();
serve_result
}