use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
use utoipa::ToSchema;
pub const ZONE_TYPE_PRIMARY: &str = "primary";
pub const ZONE_TYPE_SECONDARY: &str = "secondary";
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct SoaRecord {
pub primary_ns: String,
pub admin_email: String,
#[serde(default = "default_serial")]
pub serial: u32,
#[serde(default = "default_refresh")]
pub refresh: u32,
#[serde(default = "default_retry")]
pub retry: u32,
#[serde(default = "default_expire")]
pub expire: u32,
#[serde(default = "default_negative_ttl")]
pub negative_ttl: u32,
}
fn default_serial() -> u32 {
let now = chrono::Utc::now();
let date_part = now.format("%Y%m%d").to_string();
format!("{}01", date_part).parse().unwrap_or(2025120601)
}
fn default_refresh() -> u32 {
3600
}
fn default_retry() -> u32 {
600
}
fn default_expire() -> u32 {
604_800
}
fn default_negative_ttl() -> u32 {
86400
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DnsRecord {
pub name: String,
#[serde(rename = "type")]
pub record_type: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<u16>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ZoneConfig {
pub ttl: u32,
pub soa: SoaRecord,
pub name_servers: Vec<String>,
pub name_server_ips: std::collections::HashMap<String, String>,
#[serde(default)]
pub records: Vec<DnsRecord>,
#[serde(skip_serializing_if = "Option::is_none")]
pub also_notify: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_transfer: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub primaries: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dnssec_policy: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inline_signing: Option<bool>,
}
impl ZoneConfig {
pub fn to_zone_file(&self) -> String {
let mut zone_file = String::new();
zone_file.push_str(&format!("$TTL {}\n\n", self.ttl));
zone_file.push_str(&format!(
"@ IN SOA {} {} (\n",
self.soa.primary_ns, self.soa.admin_email
));
zone_file.push_str(&format!(" {} ; Serial\n", self.soa.serial));
zone_file.push_str(&format!(" {} ; Refresh\n", self.soa.refresh));
zone_file.push_str(&format!(" {} ; Retry\n", self.soa.retry));
zone_file.push_str(&format!(" {} ; Expire\n", self.soa.expire));
zone_file.push_str(&format!(
" {} ); Negative TTL\n\n",
self.soa.negative_ttl
));
for ns in &self.name_servers {
zone_file.push_str(&format!("@ IN NS {}\n", ns));
}
if !self.name_servers.is_empty() {
zone_file.push('\n');
}
for (ns_name, ip) in &self.name_server_ips {
zone_file.push_str(&format!("{} IN A {}\n", ns_name, ip));
}
if !self.name_server_ips.is_empty() {
zone_file.push('\n');
}
for record in &self.records {
let ttl_str = if let Some(ttl) = record.ttl {
format!("{} ", ttl)
} else {
String::new()
};
let priority_str = if let Some(priority) = record.priority {
format!("{} ", priority)
} else {
String::new()
};
zone_file.push_str(&format!(
"{} {}IN {} {}{}\n",
record.name, ttl_str, record.record_type, priority_str, record.value
));
}
zone_file
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct CreateZoneRequest {
pub zone_name: String,
pub zone_type: String,
pub zone_config: ZoneConfig,
pub update_key_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ModifyZoneRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub also_notify: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_transfer: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_update: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
pub struct ZoneResponse {
pub success: bool,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
pub struct ServerStatusResponse {
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ZoneInfo {
pub name: String,
pub zone_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub serial: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(ToSchema))]
pub struct ZoneListResponse {
pub zones: Vec<String>,
pub count: usize,
}