diff --git a/locales/en/main.ftl b/locales/en/main.ftl index 74da349..7d88824 100644 --- a/locales/en/main.ftl +++ b/locales/en/main.ftl @@ -11,18 +11,18 @@ zone-content-section-mail-header = E-mail zone-content-section-services-header = Services zone-content-section-general-header = General -zone-content-record-type-address = +zone-content-record-type-addresses = .type-name = IP addresses -zone-content-record-type-mailserver = +zone-content-record-type-mailservers = .type-name = E-mail servers .data-preference = Preference: { $preference } -zone-content-record-type-nameserver = +zone-content-record-type-nameservers = .type-name = Name servers zone-content-record-type-service = - .type-name = Services + .type-name = Service .data-priority = Priority: { $priority } .data-weight = Weight: { $weight } diff --git a/src/resources/dns/friendly/rdata.rs b/src/resources/dns/friendly/rdata.rs index 84ddcc2..0323e0e 100644 --- a/src/resources/dns/friendly/rdata.rs +++ b/src/resources/dns/friendly/rdata.rs @@ -1,190 +1,134 @@ -use std::{collections::HashMap, fmt}; +use std::{collections::HashMap, hash::Hash}; -use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct}; +use serde::{Deserialize, Serialize, Serializer}; use crate::resources::dns::internal; -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] #[serde(rename_all="lowercase")] pub enum FriendlyRType { - Address, + Addresses, Alias, - MailServer, - NameServer, + MailServers, + NameServers, Service, Spf, - TextData, -} - -impl fmt::Display for FriendlyRType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - FriendlyRType::Address => write!(f, "address"), - FriendlyRType::Alias => write!(f, "alias"), - FriendlyRType::MailServer => write!(f, "mailserver"), - FriendlyRType::NameServer => write!(f, "nameserver"), - FriendlyRType::Service => write!(f, "service"), - FriendlyRType::Spf => write!(f, "spf"), - FriendlyRType::TextData => write!(f, "textdata"), - } - } -} - -pub trait HasRType { - fn rtype(&self) -> FriendlyRType; + Texts, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all="lowercase")] +pub enum RecordSection { + Mail, + Web, + Services, + Miscellaneous, } #[derive(Debug, Deserialize, Serialize)] -#[serde(tag = "_type")] pub enum FriendlyRData { Address(Address), Alias(Alias), MailServer(MailServer), NameServer(NameServer), - Service(Service), + Service(ServiceSingleTarget), Spf(Spf), TextData(TextData), } -impl HasRType for FriendlyRData { - fn rtype(&self) -> FriendlyRType { - match self { - FriendlyRData::Address(_) => FriendlyRType::Address, - FriendlyRData::Alias(_) => FriendlyRType::Alias, - FriendlyRData::MailServer(_) => FriendlyRType::MailServer, - FriendlyRData::NameServer(_) => FriendlyRType::NameServer, - FriendlyRData::Service(_) => FriendlyRType::Service, - FriendlyRData::Spf(_) => FriendlyRType::Spf, - FriendlyRData::TextData(_) => FriendlyRType::TextData, - } - } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum FriendlyRDataAggregated { + Addresses(Addresses), + MailServers(MailServers), + NameServers(NameServers), + Service(Service), + Spf(Spf), + Texts(Texts), } #[derive(Debug)] -pub struct FriendlyRecord { +pub struct FriendlyRecord { ttl: i64, - data: T, + data: FriendlyRDataAggregated, } -impl Serialize for FriendlyRecord { +impl Serialize for FriendlyRecord { fn serialize(&self, serializer: S) -> Result where S: Serializer, { - let mut state = serializer.serialize_struct("FriendlyRecord", 3)?; - state.serialize_field("ttl", &self.ttl)?; - state.serialize_field("data", &self.data)?; - state.serialize_field("rtype", &self.data.rtype())?; - state.end() + #[derive(Serialize)] + struct ExtendedRecord<'a> { + ttl: i64, + record_type: FriendlyRType, + record_section: RecordSection, + #[serde(flatten)] + data: &'a FriendlyRDataAggregated, + } + + let extended_record = ExtendedRecord { + ttl: self.ttl, + data: &self.data, + record_type: self.record_type(), + record_section: self.record_section(), + }; + + extended_record.serialize(serializer) + } } -impl FriendlyRecord { - fn new(ttl: u32, data: T) -> Self { +impl FriendlyRecord { + pub fn new(ttl: u32, data: FriendlyRDataAggregated) -> Self { FriendlyRecord { ttl: ttl.into(), data, } } -} -#[derive(Debug)] -pub struct FriendlyRecordSet { - ttl: i64, - data: Vec, -} - -impl Serialize for FriendlyRecordSet { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut state = serializer.serialize_struct("FriendlyRecordSet", 3)?; - state.serialize_field("ttl", &self.ttl)?; - state.serialize_field("data", &self.data)?; - state.serialize_field("rtype", &self.data.first().map(|d| d.rtype().to_string()))?; - state.end() - } -} - -impl FriendlyRecordSet { - fn new(ttl: u32) -> Self { - FriendlyRecordSet { - ttl: ttl.into(), - data: Vec::new() + pub fn record_type(&self) -> FriendlyRType { + match self.data { + FriendlyRDataAggregated::Addresses(_) => FriendlyRType::Addresses, + FriendlyRDataAggregated::MailServers(_) => FriendlyRType::MailServers, + FriendlyRDataAggregated::NameServers(_) => FriendlyRType::NameServers, + FriendlyRDataAggregated::Service(_) => FriendlyRType::Service, + FriendlyRDataAggregated::Spf(_) => FriendlyRType::Spf, + FriendlyRDataAggregated::Texts(_) => FriendlyRType::Texts, } } -} -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all="lowercase")] -pub enum ConfigurationType { - Mail, - Web, -} - -#[derive(Debug, Serialize)] -pub struct MailConfiguration { - pub servers: Option>, - pub spf: Option>, -} - -impl MailConfiguration { - pub fn new() -> Self { - MailConfiguration { - servers: None, - spf: None + pub fn record_section(&self) -> RecordSection { + match self.data { + FriendlyRDataAggregated::Addresses(_) => RecordSection::Web, + FriendlyRDataAggregated::MailServers(_) => RecordSection::Mail, + FriendlyRDataAggregated::NameServers(_) => RecordSection::Miscellaneous, + FriendlyRDataAggregated::Service(_) => RecordSection::Services, + FriendlyRDataAggregated::Spf(_) => RecordSection::Mail, + FriendlyRDataAggregated::Texts(_) => RecordSection::Miscellaneous, } } } #[derive(Debug, Serialize)] -pub struct WebConfiguration { - pub addresses: Option>, +pub struct Node { + pub name: String, + pub records: Vec, } - -impl WebConfiguration { - pub fn new() -> Self { - WebConfiguration { - addresses: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct FriendlyRecordSetGroup { - owner: String, - general_records: Vec>, - #[serde(serialize_with = "as_vector")] - services: HashMap>, - mail: Option, - web: Option, -} - -fn as_vector(services: &HashMap>, ser: S) -> Result - where S: Serializer -{ - let container: Vec<_> = services.iter().collect(); - serde::Serialize::serialize(&container, ser) -} - -impl FriendlyRecordSetGroup { - pub fn new(owner: String) -> Self { - FriendlyRecordSetGroup { - owner, - general_records: Vec::new(), - services: HashMap::new(), - mail: None, - web: None, +impl Node { + fn new(name: String) -> Self { + Node { + name, + records: Vec::new(), } } } #[derive(Debug, Serialize)] pub struct FriendlyRecords { - records: Vec, + records: Vec, aliases: Vec, } @@ -201,6 +145,8 @@ impl From for FriendlyRecords { fn from(value: internal::RecordList) -> Self { let mut records = FriendlyRecords::new(); + let mut name_mapping: HashMap> = HashMap::new(); + let mut service_mapping: HashMap<(String, ServiceType), FriendlyRecord> = HashMap::new(); for record in value.records { let internal::Record { name, ttl, rdata } = record; @@ -216,100 +162,148 @@ impl From for FriendlyRecords { if let FriendlyRData::Alias(alias) = rdata { records.aliases.push(alias) } else { - - if records.records.is_empty() { - records.records.push(FriendlyRecordSetGroup::new(name)); - } else { - let group = records.records.last().unwrap(); - - if group.owner != name { - records.records.push(FriendlyRecordSetGroup::new(name)); - } - } - - let current_group = records.records.last_mut().unwrap(); + let node = name_mapping.entry(name.clone()).or_default(); match rdata { FriendlyRData::Address(address) => { - let web = current_group.web.get_or_insert_with(WebConfiguration::new); - let addresses = web.addresses.get_or_insert_with(|| FriendlyRecordSet::new(ttl)); + let addresses = node.entry(FriendlyRType::Addresses).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::Addresses(Addresses { + addresses: Vec::new() + })) + }); - addresses.data.push(address); + match addresses.data { + FriendlyRDataAggregated::Addresses(ref mut addresses) => addresses.addresses.push(address), + _ => unreachable!(), + }; }, FriendlyRData::MailServer(mailserver) => { - let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new); - let servers = mail.servers.get_or_insert_with(|| FriendlyRecordSet::new(ttl)); + let mailservers = node.entry(FriendlyRType::MailServers).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::MailServers(MailServers { + mailservers: Vec::new() + })) + }); - servers.data.push(mailserver); + match mailservers.data { + FriendlyRDataAggregated::MailServers(ref mut mailservers) => mailservers.mailservers.push(mailserver), + _ => unreachable!(), + }; }, FriendlyRData::Spf(spf) => { - let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new); - - mail.spf = Some(FriendlyRecord::new(ttl, spf)); + node.insert(FriendlyRType::Spf, FriendlyRecord::new(ttl, FriendlyRDataAggregated::Spf(spf))); }, - FriendlyRData::Service(service) => { - let services = current_group.services.entry(service.service_type.clone()).or_insert(FriendlyRecordSet::new(ttl)); + FriendlyRData::Service(service_single) => { + let service = service_mapping.entry((name.clone(), service_single.service_type.clone())) + .or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::Service(Service { + service_type: service_single.service_type, + service_targets: Vec::new(), - services.data.push(service) - } - FriendlyRData::Alias(_) => {}, - data => { + })) + }); + + match service.data { + FriendlyRDataAggregated::Service(ref mut service) => service.service_targets.push(service_single.service_target), + _ => unreachable!(), + }; + }, + FriendlyRData::NameServer(nameserver) => { // TODO: NS -> Skip if NS for zone (authority), create Delegation section with glue + DS for others (how to check if record is glue?) + let nameservers = node.entry(FriendlyRType::NameServers).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::NameServers(NameServers { + nameservers: Vec::new() + })) + }); - if current_group.general_records.is_empty() { - current_group.general_records.push(FriendlyRecordSet::new(ttl)); - } else { - let rrset = current_group.general_records.last().unwrap(); - if rrset.data.last().unwrap().rtype() != data.rtype() { - current_group.general_records.push(FriendlyRecordSet::new(ttl)); - } - } + match nameservers.data { + FriendlyRDataAggregated::NameServers(ref mut nameservers) => nameservers.nameservers.push(nameserver), + _ => unreachable!(), + }; + }, + FriendlyRData::TextData(text) => { + let texts = node.entry(FriendlyRType::Texts).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::Texts(Texts { + texts: Vec::new() + })) + }); - let current_rrset = current_group.general_records.last_mut().unwrap(); - current_rrset.data.push(data) - } + match texts.data { + FriendlyRDataAggregated::Texts(ref mut texts) => texts.texts.push(text), + _ => unreachable!(), + }; + }, + FriendlyRData::Alias(_) => {}, } } } + let mut nodes: HashMap = HashMap::new(); + + for ((name, _), service) in service_mapping { + let node = nodes.entry(name.clone()) + .or_insert_with(|| Node::new(name)); + node.records.push(service); + } + + for (name, node_records) in name_mapping { + let node = nodes.entry(name.clone()) + .or_insert_with(|| Node::new(name)); + for (_, record) in node_records { + node.records.push(record); + } + } + + records.records = nodes.into_values().collect(); + + records.records.sort_by_key(|node| node.name.clone()); + records } } /* --------- RDATA --------- */ +/* --------- Address --------- */ + #[derive(Debug, Deserialize, Serialize)] pub struct Address { pub address: String } +#[derive(Debug, Deserialize, Serialize)] +pub struct Addresses { + pub addresses: Vec
, +} -impl HasRType for Address { - fn rtype(&self) -> FriendlyRType { - FriendlyRType::Address - } +/* --------- Service --------- */ + +#[derive(Debug, Deserialize, Serialize)] +pub struct ServiceSingleTarget { + pub service_type: ServiceType, + pub service_target: ServiceTarget, } #[derive(Debug, Deserialize, Serialize)] pub struct Service { pub service_type: ServiceType, + pub service_targets: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ServiceTarget { pub port: i64, pub weight: i64, pub priority: i64, pub server: String, } -#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq, Clone)] +#[derive(Debug, Hash, Eq, PartialEq, Clone, Deserialize, Serialize)] #[serde(rename_all = "lowercase", tag = "service_type")] pub enum ServiceType { Other { protocol: String, name: String }, } -impl HasRType for Service { - fn rtype(&self) -> FriendlyRType { - FriendlyRType::Service - } -} +/* --------- MailServer --------- */ #[derive(Debug, Deserialize, Serialize)] pub struct MailServer { @@ -317,36 +311,47 @@ pub struct MailServer { pub mail_exchanger: String, } -impl HasRType for MailServer { - fn rtype(&self) -> FriendlyRType { - FriendlyRType::MailServer - } +#[derive(Debug, Deserialize, Serialize)] +pub struct MailServers { + pub mailservers: Vec } +/* --------- NameServer --------- */ + #[derive(Debug, Deserialize, Serialize)] pub struct NameServer { pub target: String, } +#[derive(Debug, Deserialize, Serialize)] +pub struct NameServers { + pub nameservers: Vec +} + +/* --------- TextData --------- */ + #[derive(Debug, Deserialize, Serialize)] pub struct TextData { pub text: String, } +#[derive(Debug, Deserialize, Serialize)] +pub struct Texts { + pub texts: Vec, +} + +/* --------- Spf --------- */ + +#[derive(Debug, Deserialize, Serialize)] +pub struct Spf { + pub policy: String +} + +/* --------- Alias --------- */ + #[derive(Debug, Deserialize, Serialize)] pub struct Alias { pub from: String, pub target: String, pub ttl: i64, } - -#[derive(Debug, Deserialize, Serialize)] -pub struct Spf { - pub policy: String -} - -impl HasRType for Spf { - fn rtype(&self) -> FriendlyRType { - FriendlyRType::Spf - } -} diff --git a/src/resources/dns/internal/rdata.rs b/src/resources/dns/internal/rdata.rs index c8380c3..ff9107d 100644 --- a/src/resources/dns/internal/rdata.rs +++ b/src/resources/dns/internal/rdata.rs @@ -147,15 +147,17 @@ impl Srv { let service_name = labels[0]. strip_prefix('_'); let protocol = labels[1]. strip_prefix('_'); if let (Some(service_name), Some(protocol)) = (service_name, protocol) { - Some((labels[2].to_string(), friendly::FriendlyRData::Service(friendly::Service { + Some((labels[2].to_string(), friendly::FriendlyRData::Service(friendly::ServiceSingleTarget { service_type: friendly::ServiceType::Other { name: service_name.into(), protocol: protocol.into() }, - port: self.port.into(), - weight: self.weight.into(), - priority: self.priority.into(), - server: self.server.to_string(), + service_target: friendly::ServiceTarget { + port: self.port.into(), + weight: self.weight.into(), + priority: self.priority.into(), + server: self.server.to_string() + } }))) } else { None diff --git a/src/routes/ui/zones.rs b/src/routes/ui/zones.rs index d6d35c8..6090b92 100644 --- a/src/routes/ui/zones.rs +++ b/src/routes/ui/zones.rs @@ -1,6 +1,6 @@ use axum::extract::{Query, Path, State, OriginalUri}; use axum::Extension; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use unic_langid::LanguageIdentifier; @@ -35,10 +35,17 @@ pub async fn get_records_page( #[derive(Deserialize)] pub struct NewRecordQuery { subdomain: Option, - config: Option, + config: Option, rtype: Option, } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ConfigurationType { + Mail, + Web, +} + pub async fn get_new_record_page( Path(zone_name): Path, State(app): State, diff --git a/templates/macros/display_rrset.html b/templates/macros/display_rrset.html index 83240db..c0a9551 100644 --- a/templates/macros/display_rrset.html +++ b/templates/macros/display_rrset.html @@ -1,81 +1,93 @@ -{% macro rrset(rtype, ttl, data, zone, lang, service_type="") %} -
  • -
    - {% if rtype == "service" %} - {% if service_type.service_type == "other" %} - {{ service_type.name }}/{{ service_type.protocol }} - {% else %} - {{ service_type.service_type }} - {% endif %} +{% macro rrset(record, zone, lang) %} +
  • +
    + {% if record.record_type == "service" %} + {% if record.service.service_type.service_type == "other" %} + {{ record.service.service_type.name }}/{{ record.service.service_type.protocol }} {% else %} - {{ tr(msg="zone-content-record-type-" ~ rtype, attr="type-name", lang=lang) }} + {{ record.srvice.service_type.service_type }} {% endif %} - + {% else %} + {{ tr(msg="zone-content-record-type-" ~ record.record_type, attr="type-name", lang=lang) }} + {% endif %} + +
    -
      - {% for data in data %} +
        + {% if record.record_type == "addresses" %} + {% for address in record.addresses.addresses %}
      • - {% if rtype == "address" %}
        - {{ data.address }} + {{ address.address }}
        - {% elif rtype == "mailserver" %} -
        - - {{ data.mail_exchanger }} - -
        -
        - - {{ tr( - msg="zone-content-record-type-mailserver", - attr="data-preference", - preference=data.preference, - lang=lang) }} - -
        - {% elif rtype == "nameserver" %} -
        - - {{ data.target }} - -
        - {% elif rtype == "service" %} -
        - - {{ data.server ~ ":" ~ data.port }} - -
        -
        - - {{ tr( - msg="zone-content-record-type-service", - attr="data-priority", - priority=data.priority, - lang=lang) }} - - - {{ tr( - msg="zone-content-record-type-service", - attr="data-weight", - weight=data.weight, - lang=lang) }} - -
        - {% endif %}
      • {% endfor %} -
      - + {% elif record.record_type == "mailservers" %} + {% for mailserver in record.mailservers.mailservers %} +
    • +
      + + {{ mailserver.mail_exchanger }} + +
      +
      + + {{ tr( + msg="zone-content-record-type-mailservers", + attr="data-preference", + preference=mailserver.preference, + lang=lang) }} + +
      +
    • + {% endfor %} + {% elif record.record_type == "nameservers" %} + {% for nameserver in record.nameservers.nameservers %} +
    • +
      + + {{ nameserver.target }} + +
      +
    • + {% endfor %} + {% elif record.record_type == "service" %} + {% for service_target in record.service.service_targets %} +
    • +
      + + {{ service_target.server ~ ":" ~ service_target.port }} + +
      +
      + + {{ tr( + msg="zone-content-record-type-service", + attr="data-priority", + priority=service_target.priority, + lang=lang) }} + + + {{ tr( + msg="zone-content-record-type-service", + attr="data-weight", + weight=service_target.weight, + lang=lang) }} + +
      +
    • + {% endfor %} + {% endif %} +
    +
  • {% endmacro rrset %} diff --git a/templates/pages/new_record.html b/templates/pages/new_record.html index f145cbe..050f2d1 100644 --- a/templates/pages/new_record.html +++ b/templates/pages/new_record.html @@ -4,131 +4,13 @@ {% block main %}

    Create a new record in zone {{ current_zone }}

    + {% if not new_record_name or (new_record_name and domain_error) %} -

    Choose the name of the new record

    -
    - -
    - - .{{ current_zone }} -
    - {% if domain_error %} -

    {{ domain_error.description }}

    - {% endif %} -

    Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".

    - -
    + {% include "pages/new_record/choose_name.html" %} {% elif not config and not rtype %} - -

    Configure the domain {{ new_record_name }}...

    - -

    ...or create a new record for the domain {{ new_record_name }}

    -

    General

    - -

    E-mails

    - -

    Security

    - -

    DNS Delegation

    - + {% include "pages/new_record/choose_record.html" %} {% else %} - -{% if config == "web" %} - -

    Configure a web site for the domain {{ new_record_name }}

    - -
    -

    Web servers

    -
    - - -
    - - -
    - -{% elif config == "mail" %} - -

    Configure e-mails for the domain {{ new_record_name }}

    - -
    -

    Mail servers

    -
    - Mail server #1 - -
    -
    - - -
    - -
    - - -
    -
    -
    - -

    Security

    - -
    - - -
    - -
    - - -
    - -
    - Cryptographic signature (DKIM) #1 - -
    -
    - - -
    - -
    - - -
    -
    -
    - - - -
    -{% endif %} - - - + {% include "pages/new_record/configure_record.html" %} {% endif %} {% endblock %} diff --git a/templates/pages/new_record/choose_name.html b/templates/pages/new_record/choose_name.html new file mode 100644 index 0000000..08b5fd9 --- /dev/null +++ b/templates/pages/new_record/choose_name.html @@ -0,0 +1,19 @@ +

    Choose the name of the new record

    +
    + +
    + + .{{ current_zone }} +
    + {% if domain_error %} +

    {{ domain_error.description }}

    + {% endif %} +

    Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".

    + +
    diff --git a/templates/pages/new_record/choose_record.html b/templates/pages/new_record/choose_record.html new file mode 100644 index 0000000..ba09731 --- /dev/null +++ b/templates/pages/new_record/choose_record.html @@ -0,0 +1,30 @@ +

    Configure the domain {{ new_record_name }}...

    + +

    ...or create a new record for the domain {{ new_record_name }}

    +

    General

    + +

    E-mails

    + +

    Security

    + +

    DNS Delegation

    + diff --git a/templates/pages/new_record/configure_record.html b/templates/pages/new_record/configure_record.html new file mode 100644 index 0000000..ffb93c6 --- /dev/null +++ b/templates/pages/new_record/configure_record.html @@ -0,0 +1,68 @@ +{% if config == "web" %} + +

    Configure a web site for the domain {{ new_record_name }}

    + +
    +

    Web servers

    +
    + + +
    + + +
    + +{% elif config == "mail" %} + +

    Configure e-mails for the domain {{ new_record_name }}

    + +
    +

    Mail servers

    +
    + Mail server #1 + +
    +
    + + +
    + +
    + + +
    +
    +
    + +

    Security

    + +
    + + +
    + +
    + + +
    + +
    + Cryptographic signature (DKIM) #1 + +
    +
    + + +
    + +
    + + +
    +
    +
    + + + +
    +{% endif %} diff --git a/templates/pages/records.html b/templates/pages/records.html index 2e2b49f..713eb5b 100644 --- a/templates/pages/records.html +++ b/templates/pages/records.html @@ -12,14 +12,15 @@ +

    {{ tr(msg="zone-content-records-header", lang=lang) }}

    - {% for group in records.records %} + {% for node in records.records %}
    -

    {{ group.owner }}

    +

    {{ node.name }}

    - +
    + {% set sections = node.records | group_by(attribute="record_section") %}
    - {% if group.web %} + {% if sections.web %} + {% set records = sections.web | group_by(attribute="record_type") %}

    {{ tr(msg="zone-content-section-web-header", lang=lang) }}

      - {% if group.web.addresses %} + {% if records.addresses %} {{ rrset::rrset( - rtype=group.web.addresses.rtype, - ttl=group.web.addresses.ttl, - data=group.web.addresses.data, + record=records.addresses.0, zone=current_zone, lang=lang) }} {% endif %}
    {% endif %} - {% if group.mail %} + {% if sections.mail %} + {% set records = sections.mail | group_by(attribute="record_type") %}

    {{ tr(msg="zone-content-section-mail-header", lang=lang) }}

      - {% if group.mail.servers %} + {% if records.mailservers %} {{ rrset::rrset( - rtype=group.mail.servers.rtype, - ttl=group.mail.servers.ttl, - data=group.mail.servers.data, + record=records.mailservers.0, zone=current_zone, lang=lang) }} {% endif %} - {% if group.mail.spf %} + {% if records.spf %} {{ rrset::rrset( - rtype=group.mail.spf.rtype, - ttl=group.mail.spf.ttl, - data=group.mail.spf.data, + record=records.spf.0, zone=current_zone, lang=lang) }} {% endif %}
    {% endif %} - {% if group.services %} + {% if sections.services %}

    {{ tr(msg="zone-content-section-services-header", lang=lang) }}

      - {% for service in group.services %} + {% for service in sections.services %} {{ rrset::rrset( - rtype=service[1].rtype, - ttl=service[1].ttl, - data=service[1].data, + record=service, zone=current_zone, - lang=lang, - service_type=service[0]) }} + lang=lang) }} {% endfor %}
    {% endif %} - {% if group.general_records %} + {% if sections.miscellaneous %}

    {{ tr(msg="zone-content-section-general-header", lang=lang) }}

      - {% for rrset in group.general_records %} + {% for record in sections.miscellaneous %} {{ rrset::rrset( - rtype=rrset.rtype, - ttl=rrset.ttl, - data=rrset.data, + record=record, zone=current_zone, lang=lang) }} {% endfor %}