Files
rust-ipam/src/main.rs
mathieu 75c13b261b feat(api): add Leptos server functions bridging client and server
- Add src/api/ module with server functions for networks, hosts, applications
- Each server function retrieves the pool via use_context::<AnyPool>()
- Pool is injected via provide_context in two places in main.rs:
  * leptos_routes_with_context: for SSR renders and inline server fn calls
  * handle_server_fns_with_context on /api/*fn_name: for WASM client calls
- create_host validates IP against network CIDR before inserting
- create_network validates CIDR format before inserting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 22:01:31 +02:00

152 lines
6.0 KiB
Rust

// main.rs — Axum server entry point
//
// This file is compiled ONLY when the "ssr" feature is enabled.
// `#[cfg(feature = "ssr")]` works like `#ifdef` in C:
// the guarded code does not exist in the WASM bundle.
//
// Run the server:
// cargo run --features ssr
//
// Run with verbose logs:
// RUST_LOG=debug cargo run --features ssr
#[cfg(feature = "ssr")]
#[tokio::main]
// `#[tokio::main]` turns the synchronous `fn main()` into an async function
// managed by the Tokio runtime. Without it, Rust cannot execute `async` code.
async fn main() {
use axum::Router;
use leptos::config::get_configuration;
use leptos::prelude::provide_context;
use leptos::view;
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use rust_ipam::{
app::{App, Shell},
server::{
config::AppConfig,
db::{create_pool, run_migrations},
routes::not_found_handler,
state::AppState,
},
};
use tower_http::services::ServeDir;
// Initialize structured logging.
// tracing::info!(), tracing::warn!(), etc. produce no output without this.
// RUST_LOG=debug cargo run --features ssr → enables debug-level logs
tracing_subscriber::fmt()
.with_env_filter(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
)
.init();
tracing::info!("Starting Rust IPAM server...");
// Load configuration from environment variables / .env file.
// The server cannot start without knowing which database to connect to,
// so we abort immediately on any configuration error.
let app_config = AppConfig::from_env()
.expect("Configuration error — check your .env file");
tracing::info!("Database: {} ({})", app_config.backend, app_config.database_url);
// Connect to the database and apply any pending migrations.
// The server cannot serve data without a working database, so we abort on error.
let pool = create_pool(&app_config)
.await
.expect("Failed to connect to database");
run_migrations(&pool, &app_config.backend)
.await
.expect("Database migration failed");
tracing::info!("Database ready.");
// `Some("Cargo.toml")` tells Leptos to read the [package.metadata.leptos]
// section from Cargo.toml (file paths, output names, server address...).
let conf = get_configuration(Some("Cargo.toml"))
.expect("Failed to load Leptos configuration from Cargo.toml");
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr;
// Combine Leptos options and the database pool into a single shared state.
// `AppState` implements `FromRef<AppState> for LeptosOptions` so Leptos
// can still extract just what it needs from the full state.
let state = AppState { leptos_options: leptos_options.clone(), db: pool };
// Walk all `<Route>` components inside `App` to build the list of URLs
// that Leptos SSR must handle.
let routes = generate_route_list(App);
// Clone the pool so we can inject it into two different contexts:
// 1. `leptos_routes_with_context` — SSR rendering + server functions called during SSR
// 2. `handle_server_fns_with_context` — server functions called from the WASM client
let pool_for_routes = state.db.clone();
let pool_for_fns = state.db.clone();
// Build the Axum router using the builder pattern (method chaining).
//
// `Router::<AppState>::new()` explicitly tells Rust the state type is `AppState`.
// Without this annotation, type inference would default to `LeptosOptions`
// (inferred from `leptos_routes`) and then reject `.with_state(state: AppState)`.
let app = Router::<AppState>::new()
// Serve static files compiled by trunk (WASM, JS...).
// Trunk places them in target/site/pkg/ as configured in [package.metadata.leptos].
.nest_service("/pkg", ServeDir::new("target/site/pkg"))
// Handle server function HTTP calls from the WASM client.
//
// `#[server]` functions register themselves at "/api/<fn-name>".
// `handle_server_fns_with_context` runs the server function body and injects
// `additional_context` into the Leptos context before execution,
// so server functions can call `use_context::<AnyPool>()` to get the pool.
.route(
"/api/*fn_name",
axum::routing::post({
let pool = pool_for_fns;
move |req| {
let pool = pool.clone();
handle_server_fns_with_context(
move || provide_context(pool.clone()),
req,
)
}
}),
)
// Mount all Leptos routes into Axum.
// `leptos_routes_with_context` injects the pool into the Leptos context
// for every SSR render — needed for server functions called during SSR
// (e.g. when a `Resource` pre-fetches data on the server).
.leptos_routes_with_context(
&state,
routes,
{
move || provide_context(pool_for_routes.clone())
},
{
let leptos_options = state.leptos_options.clone();
move || view! { <Shell options=leptos_options.clone()/> }
},
)
.fallback(not_found_handler)
// Share AppState (Leptos options + DB pool) with all handlers.
.with_state(state);
let listener = tokio::net::TcpListener::bind(&addr)
.await
.expect(&format!("Failed to bind to address {}", addr));
tracing::info!("Server listening on http://{}", addr);
axum::serve(listener, app)
.await
.expect("Fatal server error");
}
// This empty block is required so the compiler finds a `fn main()`
// when building in WASM mode (where the "ssr" feature is not enabled).
// In WASM, the real entry point is `hydrate()` in lib.rs.
#[cfg(not(feature = "ssr"))]
fn main() {}