use bindizr_core::dns::name::{email_to_soa_mailbox, to_fqdn};
use chrono::Utc;
use super::{ZoneService, validation::normalize_zone_lookup_name};
use crate::{
RepositoryTx,
error::ServiceError,
log_error, log_info, log_warn,
model::{
record::{Record, RecordType},
zone::Zone,
zone_change::ZoneChange,
},
repository::RepositoryService,
serial::generate_serial,
types::CreateZoneRequest,
zone::{snapshot::save_zone_snapshot_tx, validation::validate_create_zone_request},
};
impl ZoneService {
pub async fn update_tx(tx: &mut RepositoryTx<'_>, zone: Zone) -> Result<Zone, ServiceError> {
RepositoryService::update_zone_tx(tx, zone).await
}
pub async fn create_change_tx(
tx: &mut RepositoryTx<'_>,
zone_change: ZoneChange,
) -> Result<ZoneChange, ServiceError> {
RepositoryService::create_zone_change_tx(tx, zone_change).await
}
pub async fn update(
zone_name: &str,
update_zone_request: &CreateZoneRequest,
) -> Result<Zone, ServiceError> {
let lookup_name = normalize_zone_lookup_name(zone_name)?;
let existing_zone = match RepositoryService::get_zone_by_name(&lookup_name).await {
Ok(Some(zone)) => zone,
Ok(None) => {
log_error!("Zone with name '{}' not found", zone_name);
return Err(ServiceError::NotFound(format!(
"Zone with name '{}' not found",
zone_name
)));
}
Err(e) => {
log_error!("Failed to fetch zone: {}", e);
return Err(ServiceError::Internal("Failed to update zone".to_string()));
}
};
let zone_id = existing_zone.id;
let validated = validate_create_zone_request(update_zone_request)?;
match RepositoryService::get_zone_by_name(&validated.name).await {
Ok(Some(zone)) => {
if zone.id != zone_id {
log_error!("Zone with name {} already exists", validated.name);
return Err(ServiceError::BadRequest(
"Zone name already exists".to_string(),
));
}
}
Ok(None) => {}
Err(e) => {
log_error!("Failed to check existing zone: {}", e);
return Err(ServiceError::Internal("Failed to update zone".to_string()));
}
};
let new_serial = match update_zone_request.serial {
Some(s) => s,
None => generate_serial(Some(existing_zone.serial)),
};
let zone_records = RepositoryService::get_records_by_zone_id(zone_id)
.await
.map_err(|e| {
log_error!("Failed to fetch zone records: {}", e);
ServiceError::Internal("Failed to update zone".to_string())
})?;
let mut tx = RepositoryService::begin_tx("Failed to update zone").await?;
let apply_result = async {
let updated_zone = RepositoryService::update_zone_tx(
&mut tx,
Zone {
id: zone_id,
name: validated.name.clone(),
primary_ns: validated.primary_ns.clone(),
admin_email: validated.admin_email.clone(),
ttl: validated.ttl,
serial: new_serial,
refresh: update_zone_request.refresh.unwrap_or(86400),
retry: update_zone_request.retry.unwrap_or(7200),
expire: update_zone_request.expire.unwrap_or(3_600_000),
minimum_ttl: update_zone_request.minimum_ttl.unwrap_or(86400),
created_at: existing_zone.created_at,
},
)
.await
.map_err(|e| {
log_error!("Failed to update zone: {}", e);
ServiceError::Internal("Failed to update zone".to_string())
})?;
let has_primary_ns = zone_records.iter().any(|r| {
r.record_type == RecordType::NS
&& r.name == "@"
&& to_fqdn(&r.value).eq_ignore_ascii_case(&to_fqdn(&updated_zone.primary_ns))
});
if !has_primary_ns {
let primary_ns_record = Record {
id: 0,
name: "@".to_string(),
record_type: RecordType::NS,
value: updated_zone.primary_ns.clone(),
ttl: Some(updated_zone.ttl),
priority: None,
zone_id,
created_at: Utc::now(),
};
RepositoryService::create_record_tx(&mut tx, primary_ns_record)
.await
.map_err(|e| {
log_error!("Failed to create primary NS record during update: {}", e);
ServiceError::Internal("Failed to keep primary NS consistency".to_string())
})?;
RepositoryService::create_zone_change_tx(
&mut tx,
ZoneChange {
id: 0,
zone_id,
serial: new_serial,
operation: "ADD".to_string(),
record_name: "@".to_string(),
record_type: "NS".to_string(),
record_value: updated_zone.primary_ns.clone(),
record_ttl: Some(updated_zone.ttl),
record_priority: None,
},
)
.await
.map_err(|e| {
log_error!("Failed to create zone change (ADD NS): {}", e);
ServiceError::Internal("Failed to create zone change".to_string())
})?;
}
let format_soa = |zone: &Zone| -> Result<String, ServiceError> {
Ok(format!(
"{} {} {} {} {} {} {}",
zone.primary_ns,
email_to_soa_mailbox(&zone.admin_email)
.map_err(|e| ServiceError::BadRequest(e.to_string()))?,
zone.serial,
zone.refresh,
zone.retry,
zone.expire,
zone.minimum_ttl
))
};
RepositoryService::create_zone_change_tx(
&mut tx,
ZoneChange {
id: 0,
zone_id,
serial: new_serial,
operation: "DEL".to_string(),
record_name: "@".to_string(),
record_type: "SOA".to_string(),
record_value: format_soa(&existing_zone)?,
record_ttl: Some(existing_zone.ttl),
record_priority: None,
},
)
.await
.map_err(|e| {
log_error!("Failed to create zone change (DEL SOA): {}", e);
ServiceError::Internal("Failed to create zone change".to_string())
})?;
RepositoryService::create_zone_change_tx(
&mut tx,
ZoneChange {
id: 0,
zone_id,
serial: new_serial,
operation: "ADD".to_string(),
record_name: "@".to_string(),
record_type: "SOA".to_string(),
record_value: format_soa(&updated_zone)?,
record_ttl: Some(updated_zone.ttl),
record_priority: None,
},
)
.await
.map_err(|e| {
log_error!("Failed to create zone change (ADD SOA): {}", e);
ServiceError::Internal("Failed to create zone change".to_string())
})?;
save_zone_snapshot_tx(&mut tx, &updated_zone, new_serial).await?;
Ok::<Zone, ServiceError>(updated_zone)
}
.await;
let updated_zone =
RepositoryService::finish_tx(tx, apply_result, "Failed to update zone").await?;
log_info!(
"event=zone_update zone={} previous_name={} new_serial={} zone_id={}",
updated_zone.name,
zone_name,
new_serial,
updated_zone.id
);
if let Err(e) = crate::notify::send_notify_after_update(Some(&updated_zone.name)).await {
log_warn!(
"Failed to send NOTIFY for zone {}: {}",
updated_zone.name,
e
);
}
if existing_zone.name != updated_zone.name
&& let Err(e) = crate::notify::send_notify_after_update(Some("catalog.bind")).await
{
log_warn!("Failed to send NOTIFY for catalog.bind: {}", e);
}
Ok(updated_zone)
}
}