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>
This commit is contained in:
2026-05-15 22:01:31 +02:00
parent a352a8edfd
commit 75c13b261b
6 changed files with 314 additions and 10 deletions

77
src/api/hosts.rs Normal file
View File

@@ -0,0 +1,77 @@
// api/hosts.rs — Server functions for hosts
use leptos::prelude::*;
use crate::models::Host;
// ─── Queries ──────────────────────────────────────────────────────────────────
/// Returns all hosts belonging to a given network.
#[server]
pub async fn get_hosts_by_network(network_id: i64) -> Result<Vec<Host>, ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::hosts as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::list_hosts_by_network(&pool, network_id)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
// ─── Mutations ────────────────────────────────────────────────────────────────
/// Creates a new host inside the specified network.
///
/// Validates that `ip` falls within the CIDR of `network_id`.
/// Returns an error if the network does not exist or the IP is out of range.
#[server]
pub async fn create_host(
name: String,
ip: String,
network_id: i64,
) -> Result<Host, ServerFnError> {
use sqlx::AnyPool;
use crate::server::{
repository::{hosts, networks},
validation::validate_ip_in_network,
};
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
// Look up the parent network to get its CIDR for IP validation.
let network = networks::find_network(&pool, network_id)
.await
.map_err(|e| ServerFnError::new(e.to_string()))?
.ok_or_else(|| {
ServerFnError::new(format!("Network {network_id} not found"))
})?;
// Enforce the business rule: a host's IP must belong to its network's CIDR.
// This check runs on the server before any INSERT, so the database stays consistent.
validate_ip_in_network(&ip, &network.cidr)
.map_err(|e| ServerFnError::new(e.to_string()))?;
hosts::create_host(&pool, &name, &ip, network_id)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}
/// Deletes a host by id.
///
/// Also removes all its port associations (via `ON DELETE CASCADE`).
/// Returns `true` if the host existed and was deleted.
#[server]
pub async fn delete_host(id: i64) -> Result<bool, ServerFnError> {
use sqlx::AnyPool;
use crate::server::repository::hosts as repo;
let pool = use_context::<AnyPool>()
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
repo::delete_host(&pool, id)
.await
.map_err(|e| ServerFnError::new(e.to_string()))
}