//! WireGuard Interface repository operations. use crate::error::{DbError, Result}; use crate::models::{format_datetime, parse_datetime}; use chrono::Utc; use ipnet::IpNet; use nx9_wg_core::types::wireguard::{ Interface, InterfaceRole, WireGuardPrivateKey, WireGuardPublicKey, }; use sqlx::{Row, SqlitePool}; use std::str::FromStr; use uuid::Uuid; /// Helper to convert a database row into an `Interface` domain struct. fn row_to_interface(r: &sqlx::sqlite::SqliteRow) -> Result { let id_str: String = r.try_get("id")?; let name: String = r.try_get("name")?; let role_str: Option = r.try_get("role").ok(); let private_key_str: String = r.try_get("private_key")?; let public_key_str: String = r.try_get("public_key")?; let listen_port_i64: Option = r.try_get("listen_port")?; let ipv4_cidr_str: String = r.try_get("ipv4_cidr")?; let ipv6_cidr_str: Option = r.try_get("ipv6_cidr")?; let mtu_i64: Option = r.try_get("mtu")?; let dns: Option = r.try_get("dns")?; let enabled_i64: i64 = r.try_get("enabled")?; let pre_up: Option = r.try_get("pre_up")?; let post_up: Option = r.try_get("post_up")?; let pre_down: Option = r.try_get("pre_down")?; let post_down: Option = r.try_get("post_down")?; let created_at_str: String = r.try_get("created_at")?; let updated_at_str: String = r.try_get("updated_at")?; let id = Uuid::parse_str(&id_str) .map_err(|e| DbError::Validation(format!("invalid interface UUID '{id_str}': {e}")))?; let role = match role_str.as_deref() { Some("upstream") => InterfaceRole::Upstream, _ => InterfaceRole::Overlay, }; let address_v4 = IpNet::from_str(&ipv4_cidr_str) .map_err(|e| DbError::Validation(format!("invalid ipv4_cidr '{ipv4_cidr_str}': {e}")))?; let address_v6 = match ipv6_cidr_str { Some(s) => Some( IpNet::from_str(&s) .map_err(|e| DbError::Validation(format!("invalid ipv6_cidr '{s}': {e}")))?, ), None => None, }; Ok(Interface { id, name, role, private_key: WireGuardPrivateKey::new(private_key_str), public_key: WireGuardPublicKey::new(public_key_str), listen_port: listen_port_i64.map(|p| p as u16), address_v4, address_v6, mtu: mtu_i64.map(|m| m as u16), dns, enabled: enabled_i64 != 0, pre_up, post_up, pre_down, post_down, created_at: parse_datetime(&created_at_str)?, updated_at: parse_datetime(&updated_at_str)?, }) } /// Create a new WireGuard interface desired configuration record. pub async fn create_interface(pool: &SqlitePool, iface: &Interface) -> Result<()> { let id_str = iface.id.to_string(); let ipv4_str = iface.address_v4.to_string(); let ipv6_str = iface.address_v6.as_ref().map(|ip| ip.to_string()); let created_at_str = format_datetime(&iface.created_at); let updated_at_str = format_datetime(&iface.updated_at); sqlx::query( r#" INSERT INTO interfaces ( id, name, role, private_key, public_key, listen_port, ipv4_cidr, ipv6_cidr, mtu, dns, enabled, pre_up, post_up, pre_down, post_down, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, ) .bind(&id_str) .bind(&iface.name) .bind(iface.role.as_str()) .bind(iface.private_key.as_str()) .bind(iface.public_key.as_str()) .bind(iface.listen_port.map(|p| p as i64)) .bind(&ipv4_str) .bind(ipv6_str) .bind(iface.mtu.map(|m| m as i64)) .bind(&iface.dns) .bind(if iface.enabled { 1 } else { 0 }) .bind(&iface.pre_up) .bind(&iface.post_up) .bind(&iface.pre_down) .bind(&iface.post_down) .bind(&created_at_str) .bind(&updated_at_str) .execute(pool) .await .map_err(|e| match &e { sqlx::Error::Database(dbe) if dbe.is_unique_violation() => DbError::Conflict(format!( "Interface with name '{}' already exists", iface.name )), _ => DbError::Sqlx(e), })?; Ok(()) } /// Retrieve an interface by its UUID. pub async fn get_interface(pool: &SqlitePool, id: Uuid) -> Result> { let id_str = id.to_string(); let row = sqlx::query("SELECT * FROM interfaces WHERE id = ?") .bind(&id_str) .fetch_optional(pool) .await .map_err(DbError::Sqlx)?; match row { Some(r) => Ok(Some(row_to_interface(&r)?)), None => Ok(None), } } /// Retrieve an interface by its name. pub async fn get_interface_by_name(pool: &SqlitePool, name: &str) -> Result> { let row = sqlx::query("SELECT * FROM interfaces WHERE name = ?") .bind(name) .fetch_optional(pool) .await .map_err(DbError::Sqlx)?; match row { Some(r) => Ok(Some(row_to_interface(&r)?)), None => Ok(None), } } /// List all interfaces. pub async fn list_interfaces(pool: &SqlitePool) -> Result> { let rows = sqlx::query("SELECT * FROM interfaces ORDER BY name ASC") .fetch_all(pool) .await .map_err(DbError::Sqlx)?; let mut list = Vec::with_capacity(rows.len()); for r in rows { list.push(row_to_interface(&r)?); } Ok(list) } /// Update an interface record. pub async fn update_interface(pool: &SqlitePool, iface: &Interface) -> Result<()> { let id_str = iface.id.to_string(); let ipv4_str = iface.address_v4.to_string(); let ipv6_str = iface.address_v6.as_ref().map(|ip| ip.to_string()); let now = Utc::now().naive_utc(); let now_str = format_datetime(&now); let result = sqlx::query( r#" UPDATE interfaces SET name = ?, role = ?, private_key = ?, public_key = ?, listen_port = ?, ipv4_cidr = ?, ipv6_cidr = ?, mtu = ?, dns = ?, enabled = ?, pre_up = ?, post_up = ?, pre_down = ?, post_down = ?, updated_at = ? WHERE id = ? "#, ) .bind(&iface.name) .bind(iface.role.as_str()) .bind(iface.private_key.as_str()) .bind(iface.public_key.as_str()) .bind(iface.listen_port.map(|p| p as i64)) .bind(&ipv4_str) .bind(ipv6_str) .bind(iface.mtu.map(|m| m as i64)) .bind(&iface.dns) .bind(if iface.enabled { 1 } else { 0 }) .bind(&iface.pre_up) .bind(&iface.post_up) .bind(&iface.pre_down) .bind(&iface.post_down) .bind(&now_str) .bind(&id_str) .execute(pool) .await .map_err(DbError::Sqlx)?; if result.rows_affected() == 0 { return Err(DbError::NotFound(format!("Interface '{id_str}' not found"))); } Ok(()) } /// Delete an interface by UUID. Peers are deleted automatically via ON DELETE CASCADE. pub async fn delete_interface(pool: &SqlitePool, id: Uuid) -> Result<()> { let id_str = id.to_string(); let result = sqlx::query("DELETE FROM interfaces WHERE id = ?") .bind(&id_str) .execute(pool) .await .map_err(DbError::Sqlx)?; if result.rows_affected() == 0 { return Err(DbError::NotFound(format!("Interface '{id_str}' not found"))); } Ok(()) } /// Enable or disable an interface. pub async fn set_interface_enabled(pool: &SqlitePool, id: Uuid, enabled: bool) -> Result<()> { let id_str = id.to_string(); let now = Utc::now().naive_utc(); let now_str = format_datetime(&now); let result = sqlx::query( r#" UPDATE interfaces SET enabled = ?, updated_at = ? WHERE id = ? "#, ) .bind(if enabled { 1 } else { 0 }) .bind(&now_str) .bind(&id_str) .execute(pool) .await .map_err(DbError::Sqlx)?; if result.rows_affected() == 0 { return Err(DbError::NotFound(format!("Interface '{id_str}' not found"))); } Ok(()) }