use std::net::IpAddr; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default)] pub struct AdminConfig { pub enabled: Option, pub bind_addr: Option, pub port: Option, pub grpc_port: Option, #[serde(default, deserialize_with = "string_or_vec")] pub grpc_trusted_keys: Vec, pub cors_allow_origins: Vec, pub cors_allow_credentials: Option, } fn string_or_vec<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrVec { String(String), Vec(Vec), } Ok(match StringOrVec::deserialize(deserializer)? { StringOrVec::String(value) => value .split(',') .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) .collect(), StringOrVec::Vec(values) => values, }) } impl AdminConfig { pub fn enabled(&self) -> bool { self.enabled.unwrap_or(true) } pub fn bind_addr(&self) -> &str { self.bind_addr.as_deref().unwrap_or("0.0.0.0") } pub fn port(&self) -> u16 { self.port.unwrap_or(3004) } pub fn grpc_port(&self) -> u16 { self.grpc_port.unwrap_or(3014) } pub fn grpc_trusted_keys(&self) -> &[String] { &self.grpc_trusted_keys } pub fn cors_allow_credentials(&self) -> bool { self.cors_allow_credentials.unwrap_or(false) } pub fn is_valid(&self) -> Result<(), String> { if !self.enabled() { return Ok(()); } let bind = self.bind_addr(); if bind.parse::().is_err() { return Err(format!("admin.bind_addr is not a valid IP: {bind:?}")); } if self.port == Some(0) { return Err("admin.port must be greater than 0".to_string()); } if self.grpc_port == Some(0) { return Err("admin.grpc_port must be greater than 0".to_string()); } for origin in &self.cors_allow_origins { if !origin.starts_with("http://") && !origin.starts_with("https://") { return Err(format!( "admin.cors_allow_origins entry must be an origin URL: {origin:?}" )); } } Ok(()) } }