#[cfg(all(feature = "postgres", feature = "mcp"))]
use ares::mcp::McpRegistry;
#[cfg(feature = "postgres")]
use ares::{
api,
auth::jwt::AuthService,
cli::{init, output::Output, AgentCommands, Cli, Commands},
db::PostgresClient,
utils::toml_config::AresConfig,
AgentRegistry, AppState, AresConfigManager, ConfigBasedLLMFactory, DynamicConfigManager,
ProviderRegistry, ToolRegistry,
};
#[cfg(feature = "postgres")]
use axum::{routing::get, Router};
#[cfg(feature = "postgres")]
use std::sync::Arc;
#[cfg(feature = "postgres")]
use tower_http::{cors::CorsLayer, trace::TraceLayer};
#[cfg(feature = "postgres")]
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[cfg(all(feature = "postgres", feature = "swagger-ui"))]
use utoipa::OpenApi;
#[cfg(all(feature = "postgres", feature = "swagger-ui"))]
use utoipa_swagger_ui::SwaggerUi;
#[cfg(not(feature = "postgres"))]
fn main() {
eprintln!(
"ares-server binary requires the `postgres` feature. \
Rebuild with `--features postgres` to run the server, \
or import `ares` as a library without this binary."
);
std::process::exit(1);
}
#[cfg(feature = "postgres")]
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse_args();
let output = if cli.no_color {
Output::no_color()
} else {
Output::new()
};
match cli.command {
Some(Commands::Init {
path,
force,
minimal,
no_examples,
provider,
host,
port,
}) => {
let config = init::InitConfig {
path,
force,
minimal,
no_examples,
provider,
host,
port,
};
match init::run(config, &output) {
init::InitResult::Success => std::process::exit(0),
init::InitResult::AlreadyExists => std::process::exit(1),
init::InitResult::Error(_) => std::process::exit(1),
}
}
Some(Commands::Config { full, validate }) => {
handle_config_command(&cli.config, full, validate, &output)?;
return Ok(());
}
Some(Commands::Agent(agent_cmd)) => {
handle_agent_command(&cli.config, agent_cmd, &output)?;
return Ok(());
}
None => {
#[cfg(feature = "mcp")]
if cli.mcp {
run_mcp_server(&cli.config).await?;
} else {
run_server(&cli.config, cli.verbose).await?;
}
#[cfg(not(feature = "mcp"))]
{
if cli.mcp {
eprintln!("MCP feature is not enabled. Rebuild with --features mcp");
std::process::exit(1);
}
run_server(&cli.config, cli.verbose).await?;
}
}
}
Ok(())
}
#[cfg(feature = "postgres")]
fn handle_config_command(
config_path: &std::path::Path,
full: bool,
validate: bool,
output: &Output,
) -> Result<(), Box<dyn std::error::Error>> {
output.banner();
if !config_path.exists() {
output.error(&format!(
"Configuration file '{}' not found!",
config_path.display()
));
output.hint("Run 'ares-server init' to create a new configuration");
return Err("Config not found".into());
}
let config = AresConfig::load_unchecked(config_path)?;
if validate {
output.success("Configuration is valid!");
output.newline();
}
output.header("Configuration Summary");
output.newline();
output.kv("Config file", config_path.to_str().unwrap_or("ares.toml"));
output.kv(
"Server",
&format!("{}:{}", config.server.host, config.server.port),
);
output.kv("Log level", &config.server.log_level);
output.newline();
output.subheader("Providers");
for provider_name in config.providers.keys() {
output.list_item(provider_name);
}
output.subheader("Models");
for model_name in config.models.keys() {
output.list_item(model_name);
}
output.subheader("Agents");
for agent_name in config.agents.keys() {
output.list_item(agent_name);
}
output.subheader("Tools");
for tool_name in config.enabled_tools() {
output.list_item(tool_name);
}
if full {
output.subheader("Workflows");
for workflow_name in config.workflows.keys() {
output.list_item(workflow_name);
}
}
Ok(())
}
#[cfg(feature = "postgres")]
fn handle_agent_command(
config_path: &std::path::Path,
cmd: AgentCommands,
output: &Output,
) -> Result<(), Box<dyn std::error::Error>> {
output.banner();
if !config_path.exists() {
output.error(&format!(
"Configuration file '{}' not found!",
config_path.display()
));
output.hint("Run 'ares-server init' to create a new configuration");
return Err("Config not found".into());
}
let config = AresConfig::load_unchecked(config_path)?;
match cmd {
AgentCommands::List => {
output.header("Configured Agents");
output.newline();
output.table_header(&["Name", "Model", "Tools"]);
for (name, agent) in &config.agents {
let tools = agent.tools.join(", ");
let tools_display = if tools.is_empty() { "-" } else { &tools };
output.table_row(&[name, &agent.model, tools_display]);
}
}
AgentCommands::Show { name } => {
if let Some(agent) = config.agents.get(&name) {
output.header(&format!("Agent: {}", name));
output.newline();
output.kv("Model", &agent.model);
output.kv(
"Max tool iterations",
&agent.max_tool_iterations.to_string(),
);
output.kv("Parallel tools", &agent.parallel_tools.to_string());
if !agent.tools.is_empty() {
output.subheader("Tools");
for tool in &agent.tools {
output.list_item(tool);
}
}
output.subheader("System Prompt");
if let Some(prompt) = &agent.system_prompt {
println!("{}", prompt);
} else {
println!("(no custom system prompt)");
}
} else {
output.error(&format!("Agent '{}' not found", name));
output.hint("Use 'ares-server agent list' to see available agents");
}
}
}
Ok(())
}
#[cfg(feature = "postgres")]
fn init_tracing(log_filter: &str) {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| log_filter.into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
}
#[cfg(feature = "postgres")]
async fn run_server(
config_path: &std::path::Path,
verbose: bool,
) -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let log_filter = if verbose { "debug,ares=trace" } else { "info" };
init_tracing(log_filter);
tracing::info!("Starting A.R.E.S - Agentic Retrieval Enhanced Server");
if !config_path.exists() {
let output = Output::new();
output.banner();
output.error(&format!(
"Configuration file '{}' not found!",
config_path.display()
));
output.newline();
output.info("A.R.E.S requires a configuration file to run.");
output.info("You can create one by running:");
output.newline();
output.command("ares-server init");
output.newline();
output.hint("This will create ares.toml and all necessary configuration files");
std::process::exit(1);
}
let config_path_str = config_path.to_str().unwrap_or("ares.toml");
let mut config_manager = AresConfigManager::new(config_path_str)
.expect("Failed to load configuration - check for syntax errors");
config_manager
.start_watching()
.expect("Failed to start config file watcher");
let config_manager = Arc::new(config_manager);
let config = config_manager.config();
tracing::info!(
"Configuration loaded from {} (hot-reload enabled)",
config_path_str
);
let provider_registry = Arc::new(ProviderRegistry::from_config(&config));
tracing::info!(
"Provider registry initialized with {} providers, {} models",
config.providers.len(),
config.models.len()
);
let llm_factory = Arc::new(
ConfigBasedLLMFactory::from_config(&config)
.expect("Failed to create LLM factory from config"),
);
tracing::info!(
"LLM factory initialized with default model: {}",
llm_factory.default_model()
);
let db = init_postgres_db(&config.database.url).await?;
tracing::info!("PostgreSQL database client initialized");
sqlx::migrate!("./migrations")
.run(&db.pool)
.await
.expect("Failed to run database migrations");
tracing::info!("Database migrations applied");
ares::db::tenant_agents::seed_default_templates(&db.pool)
.await
.expect("Failed to seed agent templates");
tracing::info!("Agent templates seeded");
let jwt_secret = config
.jwt_secret()
.expect("JWT_SECRET environment variable must be set");
let auth_service = AuthService::new(
jwt_secret,
config.auth.jwt_access_expiry,
config.auth.jwt_refresh_expiry,
);
tracing::info!("Auth service initialized");
let mut tool_registry = ToolRegistry::with_config(&config);
tool_registry.register(Arc::new(ares::tools::calculator::Calculator));
#[cfg(feature = "search-tools")]
tool_registry.register(Arc::new(ares::tools::search::WebSearch::new()));
#[cfg(feature = "search-tools")]
tool_registry.register(Arc::new(ares::tools::web_scrape::WebScrape::new()));
#[cfg(feature = "mcp")]
{
if let Ok(mcp_reg) = ares::mcp::McpRegistry::from_dir(config.config.mcps_dir.to_string_lossy().as_ref()) {
for client_name in mcp_reg.client_names() {
if let Some(client) = mcp_reg.get_client(&client_name) {
ares::tools::mcp_bridge::register_mcp_tools(&mut tool_registry, &client_name, client.clone());
}
}
}
}
let tool_registry = Arc::new(tool_registry);
tracing::info!(
"Tool registry initialized with {} tools",
tool_registry.enabled_tool_names().len()
);
let dynamic_config = match DynamicConfigManager::from_config(&config) {
Ok(dm) => {
tracing::info!(
"Dynamic config manager initialized with {} agents, {} models, {} tools",
dm.agents().len(),
dm.models().len(),
dm.tools().len()
);
Arc::new(dm)
}
Err(e) => {
tracing::warn!(
"Failed to initialize dynamic config manager: {}. Using empty config.",
e
);
Arc::new(
DynamicConfigManager::new(
std::path::PathBuf::from(&config.config.agents_dir),
std::path::PathBuf::from(&config.config.models_dir),
std::path::PathBuf::from(&config.config.tools_dir),
std::path::PathBuf::from(&config.config.workflows_dir),
std::path::PathBuf::from(&config.config.mcps_dir),
false,
)
.unwrap_or_else(|_| panic!("Cannot create even empty DynamicConfigManager")),
)
}
};
let agent_registry = AgentRegistry::with_dynamic_config(
&config,
Arc::clone(&provider_registry),
Arc::clone(&tool_registry),
Arc::clone(&dynamic_config),
);
let agent_registry = Arc::new(agent_registry);
tracing::info!(
"Agent registry initialized with {} agents (TOML + TOON)",
agent_registry.agent_names().len()
);
#[cfg(feature = "mcp")]
let mcp_registry: Option<Arc<McpRegistry>> =
match McpRegistry::from_dir(config.config.mcps_dir.to_string_lossy().as_ref()) {
Ok(registry) => {
tracing::info!(
"MCP registry initialized with {} clients",
registry.client_names().len()
);
Some(Arc::new(registry))
}
Err(e) => {
tracing::warn!("Failed to initialize MCP registry: {}", e);
None
}
};
let db_arc = Arc::new(db);
let tenant_db = Arc::new(ares::TenantDb::new(db_arc.clone()));
let state = AppState {
config_manager: Arc::clone(&config_manager),
db: db_arc.clone(),
tenant_db,
llm_factory,
provider_registry,
agent_registry,
tool_registry,
auth_service: Arc::new(auth_service),
dynamic_config,
#[cfg(feature = "mcp")]
mcp_registry,
deploy_registry: ares::api::handlers::deploy::new_deploy_registry(),
emergency_stop: Arc::new(std::sync::atomic::AtomicBool::new(false)),
context_provider: Arc::new(ares::agents::NoOpContextProvider),
};
{
let pool = state.tenant_db.pool().clone();
let startup_agents = state.dynamic_config.agents();
if !startup_agents.is_empty() {
if let Err(e) = ares::db::agent_versions::record_agent_versions(
&pool,
&startup_agents,
"startup",
)
.await
{
tracing::warn!("Failed to snapshot agent versions on startup: {}", e);
} else {
tracing::info!(
count = startup_agents.len(),
"Agent configs snapshotted to agent_config_versions"
);
}
}
let (version_tx, mut version_rx) =
tokio::sync::mpsc::unbounded_channel::<Vec<ares::utils::toon_config::ToonAgentConfig>>();
state.dynamic_config.set_version_tx(version_tx);
tokio::spawn(async move {
while let Some(agents) = version_rx.recv().await {
if let Err(e) = ares::db::agent_versions::record_agent_versions(
&pool,
&agents,
"hot_reload",
)
.await
{
tracing::warn!("Failed to record hot-reload agent versions: {}", e);
}
}
});
}
#[cfg(all(
feature = "swagger-ui",
feature = "local-embeddings",
feature = "ares-vector"
))]
#[derive(OpenApi)]
#[openapi(
paths(
// Auth endpoints
ares::api::handlers::auth::register,
ares::api::handlers::auth::login,
ares::api::handlers::auth::logout,
ares::api::handlers::auth::refresh_token,
// Chat endpoints
ares::api::handlers::chat::chat,
ares::api::handlers::chat::chat_stream,
ares::api::handlers::chat::get_user_memory,
// Research endpoints
ares::api::handlers::research::deep_research,
// Conversation endpoints
ares::api::handlers::conversations::list_conversations,
ares::api::handlers::conversations::get_conversation,
ares::api::handlers::conversations::update_conversation,
ares::api::handlers::conversations::delete_conversation,
// RAG endpoints
ares::api::handlers::rag::ingest,
ares::api::handlers::rag::search,
ares::api::handlers::rag::delete_collection,
ares::api::handlers::rag::list_collections,
),
components(schemas(
ares::types::ChatRequest,
ares::types::ChatResponse,
ares::types::ResearchRequest,
ares::types::ResearchResponse,
ares::types::LoginRequest,
ares::types::RegisterRequest,
ares::types::TokenResponse,
ares::types::AgentType,
ares::types::Source,
ares::api::handlers::auth::RefreshTokenRequest,
ares::api::handlers::auth::LogoutRequest,
ares::api::handlers::auth::LogoutResponse,
ares::api::handlers::conversations::ConversationSummary,
ares::api::handlers::conversations::ConversationDetails,
ares::api::handlers::conversations::ConversationMessage,
ares::api::handlers::conversations::UpdateConversationRequest,
)),
tags(
(name = "auth", description = "Authentication endpoints"),
(name = "chat", description = "Chat endpoints"),
(name = "research", description = "Research endpoints"),
(name = "conversations", description = "Conversation management endpoints"),
(name = "rag", description = "RAG (Retrieval Augmented Generation) endpoints"),
),
info(
title = "A.R.E.S - Agentic Retrieval Enhanced Server API",
version = "0.3.0",
description = "Production-grade agentic chatbot server with multi-provider LLM support"
)
)]
struct ApiDoc;
#[cfg(all(
feature = "swagger-ui",
not(all(feature = "local-embeddings", feature = "ares-vector"))
))]
#[derive(OpenApi)]
#[openapi(
paths(
// Auth endpoints
ares::api::handlers::auth::register,
ares::api::handlers::auth::login,
ares::api::handlers::auth::logout,
ares::api::handlers::auth::refresh_token,
// Chat endpoints
ares::api::handlers::chat::chat,
ares::api::handlers::chat::chat_stream,
ares::api::handlers::chat::get_user_memory,
// Research endpoints
ares::api::handlers::research::deep_research,
// Conversation endpoints
ares::api::handlers::conversations::list_conversations,
ares::api::handlers::conversations::get_conversation,
ares::api::handlers::conversations::update_conversation,
ares::api::handlers::conversations::delete_conversation,
),
components(schemas(
ares::types::ChatRequest,
ares::types::ChatResponse,
ares::types::ResearchRequest,
ares::types::ResearchResponse,
ares::types::LoginRequest,
ares::types::RegisterRequest,
ares::types::TokenResponse,
ares::types::AgentType,
ares::types::Source,
ares::api::handlers::auth::RefreshTokenRequest,
ares::api::handlers::auth::LogoutRequest,
ares::api::handlers::auth::LogoutResponse,
ares::api::handlers::conversations::ConversationSummary,
ares::api::handlers::conversations::ConversationDetails,
ares::api::handlers::conversations::ConversationMessage,
ares::api::handlers::conversations::UpdateConversationRequest,
)),
tags(
(name = "auth", description = "Authentication endpoints"),
(name = "chat", description = "Chat endpoints"),
(name = "research", description = "Research endpoints"),
(name = "conversations", description = "Conversation management endpoints"),
),
info(
title = "A.R.E.S - Agentic Retrieval Enhanced Server API",
version = "0.3.0",
description = "Production-grade agentic chatbot server with multi-provider LLM support"
)
)]
struct ApiDoc;
#[allow(unused_mut)]
let mut app = Router::new()
.route("/health", get(health_check))
.route("/health/detailed", get(health_check_detailed))
.route("/config/info", get(config_info))
.nest(
"/api",
api::routes::create_router(state.auth_service.clone(), state.tenant_db.clone()),
);
#[cfg(feature = "swagger-ui")]
{
app = app
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
tracing::info!("Swagger UI enabled - available at /swagger-ui");
}
#[cfg(feature = "ui")]
{
app = app.nest("", ui_routes());
tracing::info!("UI enabled - available at /");
}
let cors = build_cors_layer(&config.server.cors_origins);
let app = if config.server.rate_limit_per_second > 0 {
use std::sync::Arc;
use std::time::Duration;
use tower_governor::{governor::GovernorConfigBuilder, GovernorLayer};
let governor_conf = Arc::new(
GovernorConfigBuilder::default()
.per_second(config.server.rate_limit_per_second as u64)
.burst_size(config.server.rate_limit_burst)
.use_headers() .finish()
.expect("Failed to build rate limiter configuration"),
);
let governor_limiter = governor_conf.limiter().clone();
let cleanup_interval = Duration::from_secs(60);
tokio::spawn(async move {
let mut interval = tokio::time::interval(cleanup_interval);
loop {
interval.tick().await;
tracing::debug!(
"Rate limiter storage size: {}, cleaning up old entries",
governor_limiter.len()
);
governor_limiter.retain_recent();
}
});
tracing::info!(
"Rate limiting enabled: {} req/sec per IP with burst of {}",
config.server.rate_limit_per_second,
config.server.rate_limit_burst
);
app.layer(GovernorLayer::new(governor_conf))
.layer(cors)
.layer(TraceLayer::new_for_http())
.with_state(state)
} else {
tracing::warn!("Rate limiting is disabled - not recommended for production");
app.layer(cors)
.layer(TraceLayer::new_for_http())
.with_state(state)
};
let addr = format!("{}:{}", config.server.host, config.server.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("Server running on http://{}", addr);
tracing::info!("Swagger UI available at http://{}/swagger-ui/", addr);
#[cfg(feature = "ui")]
tracing::info!("Web UI available at http://{}/", addr);
let server = axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal());
server.await?;
tracing::info!("Server shut down gracefully");
Ok(())
}
#[cfg(feature = "postgres")]
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
tracing::info!("Received Ctrl+C, initiating graceful shutdown...");
}
_ = terminate => {
tracing::info!("Received SIGTERM, initiating graceful shutdown...");
}
}
}
#[cfg(all(feature = "postgres", feature = "mcp"))]
async fn run_mcp_server(config_path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
init_tracing("info");
tracing::info!("Starting A.R.E.S MCP Server");
let config_path_str = config_path.to_str().unwrap_or("ares.toml");
let config = AresConfig::load_unchecked(config_path_str)?;
let db = init_postgres_db(&config.database.url).await?;
let pool = db.pool.clone();
let tenant_db = Arc::new(ares::TenantDb::new(Arc::new(db)));
let ares_api_url = std::env::var("ARES_API_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
tracing::info!("ARES API URL: {}", ares_api_url);
ares::mcp::start_mcp_server(tenant_db, pool, &ares_api_url).await?;
Ok(())
}
#[cfg(feature = "postgres")]
async fn init_postgres_db(url: &str) -> Result<PostgresClient, Box<dyn std::error::Error>> {
tracing::info!(database_url = %url, "Initializing PostgreSQL database");
Ok(PostgresClient::new_local(url).await?)
}
#[cfg(feature = "postgres")]
fn build_cors_layer(origins: &[String]) -> CorsLayer {
use axum::http::{header, Method};
use tower_http::cors::AllowOrigin;
let (allow_origin, allow_credentials) = if origins.len() == 1 && origins[0] == "*" {
tracing::warn!(
"CORS is configured to allow all origins (*) - not recommended for production"
);
(AllowOrigin::any(), false)
} else if origins.is_empty() {
tracing::warn!("No CORS origins configured, defaulting to allow all");
(AllowOrigin::any(), false)
} else {
tracing::info!("CORS configured for origins: {:?}", origins);
(
AllowOrigin::list(origins.iter().filter_map(|o| o.parse().ok())),
true,
)
};
CorsLayer::new()
.allow_origin(allow_origin)
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::OPTIONS,
Method::PATCH,
])
.allow_headers([
header::AUTHORIZATION,
header::CONTENT_TYPE,
header::ACCEPT,
header::ORIGIN,
axum::http::HeaderName::from_static("x-admin-secret"),
])
.allow_credentials(allow_credentials)
}
#[cfg(feature = "postgres")]
async fn health_check() -> &'static str {
"OK"
}
#[cfg(feature = "postgres")]
async fn health_check_detailed(
axum::extract::State(state): axum::extract::State<AppState>,
) -> axum::Json<serde_json::Value> {
use std::time::Instant;
let start = Instant::now();
let db_status = serde_json::json!({ "status": "healthy" });
let providers: Vec<String> = state
.config_manager
.config()
.providers
.keys()
.cloned()
.collect();
let agents: Vec<String> = state
.config_manager
.config()
.agents
.keys()
.cloned()
.collect();
let elapsed_ms = start.elapsed().as_millis();
let db_healthy = db_status
.get("status")
.and_then(|s| s.as_str())
.map(|s| s == "healthy")
.unwrap_or(false);
let overall_status = if db_healthy { "healthy" } else { "degraded" };
axum::Json(serde_json::json!({
"status": overall_status,
"version": env!("CARGO_PKG_VERSION"),
"checks": {
"database": db_status,
},
"providers": providers,
"agents": agents,
"latency_ms": elapsed_ms,
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
#[cfg(feature = "postgres")]
async fn config_info(
axum::extract::State(state): axum::extract::State<AppState>,
) -> axum::Json<serde_json::Value> {
let config = state.config_manager.config();
axum::Json(serde_json::json!({
"server": {
"host": config.server.host,
"port": config.server.port,
"log_level": config.server.log_level,
},
"providers": config.providers.keys().collect::<Vec<_>>(),
"models": config.models.keys().collect::<Vec<_>>(),
"agents": config.agents.keys().collect::<Vec<_>>(),
"tools": config.enabled_tools(),
"workflows": config.workflows.keys().collect::<Vec<_>>(),
"ui_enabled": cfg!(feature = "ui"),
}))
}
#[cfg(all(feature = "postgres", feature = "ui"))]
mod ui {
use axum::{
body::Body,
http::{header, StatusCode, Uri},
response::Response,
routing::get,
Router,
};
use rust_embed::Embed;
use crate::AppState;
#[derive(Embed)]
#[folder = "ui/dist/"]
struct UiAssets;
pub fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index_handler))
.route("/*path", get(static_handler))
}
async fn index_handler() -> Response {
serve_file("index.html")
}
async fn static_handler(uri: Uri) -> Response {
let path = uri.path().trim_start_matches('/');
if let Some(asset) = UiAssets::get(path) {
return build_response(path, &asset.data);
}
if !path.contains('.') {
if let Some(asset) = UiAssets::get("index.html") {
return build_response("index.html", &asset.data);
}
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not Found"))
.unwrap()
}
fn serve_file(path: &str) -> Response {
match UiAssets::get(path) {
Some(asset) => build_response(path, &asset.data),
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("Not Found"))
.unwrap(),
}
}
fn build_response(path: &str, data: &[u8]) -> Response {
let mime = mime_guess::from_path(path).first_or_octet_stream();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime.as_ref())
.header(header::CACHE_CONTROL, "public, max-age=3600")
.body(Body::from(data.to_vec()))
.unwrap()
}
}
#[cfg(all(feature = "postgres", feature = "ui"))]
fn ui_routes() -> Router<AppState> {
ui::routes()
}