Show a modal before deleting a network. If the network has hosts, display a warning with the exact count since they will be cascade-deleted. Host count comes from the existing NetworkWithCounts data (no extra query). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
4.7 KiB
Rust
126 lines
4.7 KiB
Rust
// api/networks.rs — Server functions for networks
|
|
|
|
use leptos::prelude::*;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::models::Network;
|
|
|
|
// Network row augmented with pre-computed counts.
|
|
// Defined here (not in models.rs) because it is a presentation model
|
|
// specific to the Networks page, not a pure domain entity.
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
pub struct NetworkWithCounts {
|
|
pub id: i64,
|
|
pub cidr: String,
|
|
/// Number of hosts whose IP falls within this network's CIDR range.
|
|
pub host_count: i64,
|
|
/// Number of distinct applications linked via ports open on hosts in this network.
|
|
pub application_count: i64,
|
|
}
|
|
|
|
// ─── Queries ──────────────────────────────────────────────────────────────────
|
|
|
|
/// Returns all networks from the database.
|
|
///
|
|
/// Called by the Networks page to populate the list.
|
|
#[server]
|
|
pub async fn get_networks() -> Result<Vec<Network>, ServerFnError> {
|
|
use sqlx::AnyPool;
|
|
use crate::server::repository::networks as repo;
|
|
|
|
// `use_context` retrieves a value previously registered with `provide_context`.
|
|
// The pool was injected in main.rs before every request.
|
|
// `ok_or_else` converts `None` into an error (defensive: should never happen).
|
|
let pool = use_context::<AnyPool>()
|
|
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
|
|
|
|
// Propagate any DB error as a ServerFnError so the client sees a clean message.
|
|
repo::list_networks(&pool)
|
|
.await
|
|
.map_err(|e| ServerFnError::new(e.to_string()))
|
|
}
|
|
|
|
/// Returns all networks enriched with host and application counts.
|
|
///
|
|
/// A single SQL query fetches everything at once using correlated subqueries,
|
|
/// avoiding N+1 round-trips regardless of the number of networks.
|
|
///
|
|
/// `application_count` = distinct applications whose registered ports appear
|
|
/// among the ports open on hosts in each network (via host_ports → application_ports).
|
|
#[server]
|
|
pub async fn get_networks_with_counts() -> Result<Vec<NetworkWithCounts>, ServerFnError> {
|
|
use sqlx::{AnyPool, Row};
|
|
|
|
let pool = use_context::<AnyPool>()
|
|
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
|
|
|
|
let rows = sqlx::query(
|
|
"SELECT
|
|
n.id,
|
|
n.cidr,
|
|
(SELECT COUNT(*) FROM hosts WHERE network_id = n.id) AS host_count,
|
|
(SELECT COUNT(DISTINCT ap.application_id)
|
|
FROM hosts h
|
|
JOIN host_ports hp ON hp.host_id = h.id
|
|
JOIN application_ports ap ON ap.port_number = hp.port_number
|
|
WHERE h.network_id = n.id) AS application_count
|
|
FROM networks n
|
|
ORDER BY n.id",
|
|
)
|
|
.fetch_all(&pool)
|
|
.await
|
|
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
|
|
let networks = rows
|
|
.into_iter()
|
|
.map(|row| NetworkWithCounts {
|
|
id: row.get("id"),
|
|
cidr: row.get("cidr"),
|
|
host_count: row.get("host_count"),
|
|
application_count: row.get("application_count"),
|
|
})
|
|
.collect();
|
|
|
|
Ok(networks)
|
|
}
|
|
|
|
// ─── Mutations ────────────────────────────────────────────────────────────────
|
|
|
|
/// Creates a new network with the given CIDR block.
|
|
///
|
|
/// Returns the created record (with its auto-generated id).
|
|
/// Returns an error if the CIDR is malformed or already exists.
|
|
#[server]
|
|
pub async fn create_network(cidr: String) -> Result<Network, ServerFnError> {
|
|
use sqlx::AnyPool;
|
|
use crate::server::{repository::networks as repo, validation::validate_cidr};
|
|
|
|
let pool = use_context::<AnyPool>()
|
|
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
|
|
|
|
// Validate the CIDR before touching the database.
|
|
// Example of a valid CIDR: "192.168.1.0/24"
|
|
validate_cidr(&cidr).map_err(|e| ServerFnError::new(e.to_string()))?;
|
|
|
|
repo::create_network(&pool, &cidr)
|
|
.await
|
|
.map_err(|e| ServerFnError::new(e.to_string()))
|
|
}
|
|
|
|
/// Deletes a network by id.
|
|
///
|
|
/// Also deletes all hosts in that network (via `ON DELETE CASCADE`).
|
|
/// Returns `true` if the network existed and was deleted.
|
|
#[server]
|
|
pub async fn delete_network(id: i64) -> Result<bool, ServerFnError> {
|
|
use sqlx::AnyPool;
|
|
use crate::server::repository::networks as repo;
|
|
|
|
let pool = use_context::<AnyPool>()
|
|
.ok_or_else(|| ServerFnError::new("Database pool not found in context"))?;
|
|
|
|
repo::delete_network(&pool, id)
|
|
.await
|
|
.map_err(|e| ServerFnError::new(e.to_string()))
|
|
}
|