change record data structure

This commit is contained in:
Hannaeko 2025-05-11 13:40:35 +02:00
parent 91bffe153a
commit cfdd9afc0e
10 changed files with 440 additions and 422 deletions

View file

@ -11,18 +11,18 @@ zone-content-section-mail-header = E-mail
zone-content-section-services-header = Services zone-content-section-services-header = Services
zone-content-section-general-header = General zone-content-section-general-header = General
zone-content-record-type-address = zone-content-record-type-addresses =
.type-name = IP addresses .type-name = IP addresses
zone-content-record-type-mailserver = zone-content-record-type-mailservers =
.type-name = E-mail servers .type-name = E-mail servers
.data-preference = Preference: { $preference } .data-preference = Preference: { $preference }
zone-content-record-type-nameserver = zone-content-record-type-nameservers =
.type-name = Name servers .type-name = Name servers
zone-content-record-type-service = zone-content-record-type-service =
.type-name = Services .type-name = Service
.data-priority = Priority: { $priority } .data-priority = Priority: { $priority }
.data-weight = Weight: { $weight } .data-weight = Weight: { $weight }

View file

@ -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; use crate::resources::dns::internal;
#[derive(Debug, Deserialize, Serialize, PartialEq)] #[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq)]
#[serde(rename_all="lowercase")] #[serde(rename_all="lowercase")]
pub enum FriendlyRType { pub enum FriendlyRType {
Address, Addresses,
Alias, Alias,
MailServer, MailServers,
NameServer, NameServers,
Service, Service,
Spf, Spf,
TextData, Texts,
} }
impl fmt::Display for FriendlyRType { #[derive(Debug, Deserialize, Serialize)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { #[serde(rename_all="lowercase")]
match self { pub enum RecordSection {
FriendlyRType::Address => write!(f, "address"), Mail,
FriendlyRType::Alias => write!(f, "alias"), Web,
FriendlyRType::MailServer => write!(f, "mailserver"), Services,
FriendlyRType::NameServer => write!(f, "nameserver"), Miscellaneous,
FriendlyRType::Service => write!(f, "service"),
FriendlyRType::Spf => write!(f, "spf"),
FriendlyRType::TextData => write!(f, "textdata"),
}
}
}
pub trait HasRType {
fn rtype(&self) -> FriendlyRType;
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "_type")]
pub enum FriendlyRData { pub enum FriendlyRData {
Address(Address), Address(Address),
Alias(Alias), Alias(Alias),
MailServer(MailServer), MailServer(MailServer),
NameServer(NameServer), NameServer(NameServer),
Service(Service), Service(ServiceSingleTarget),
Spf(Spf), Spf(Spf),
TextData(TextData), TextData(TextData),
} }
impl HasRType for FriendlyRData { #[derive(Debug, Deserialize, Serialize)]
fn rtype(&self) -> FriendlyRType { #[serde(rename_all = "lowercase")]
match self { pub enum FriendlyRDataAggregated {
FriendlyRData::Address(_) => FriendlyRType::Address, Addresses(Addresses),
FriendlyRData::Alias(_) => FriendlyRType::Alias, MailServers(MailServers),
FriendlyRData::MailServer(_) => FriendlyRType::MailServer, NameServers(NameServers),
FriendlyRData::NameServer(_) => FriendlyRType::NameServer, Service(Service),
FriendlyRData::Service(_) => FriendlyRType::Service, Spf(Spf),
FriendlyRData::Spf(_) => FriendlyRType::Spf, Texts(Texts),
FriendlyRData::TextData(_) => FriendlyRType::TextData,
}
}
} }
#[derive(Debug)] #[derive(Debug)]
pub struct FriendlyRecord<T> { pub struct FriendlyRecord {
ttl: i64, ttl: i64,
data: T, data: FriendlyRDataAggregated,
} }
impl<T: Serialize + HasRType> Serialize for FriendlyRecord<T> { impl Serialize for FriendlyRecord {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
let mut state = serializer.serialize_struct("FriendlyRecord", 3)?; #[derive(Serialize)]
state.serialize_field("ttl", &self.ttl)?; struct ExtendedRecord<'a> {
state.serialize_field("data", &self.data)?; ttl: i64,
state.serialize_field("rtype", &self.data.rtype())?; record_type: FriendlyRType,
state.end() 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<T> FriendlyRecord<T> { impl FriendlyRecord {
fn new(ttl: u32, data: T) -> Self { pub fn new(ttl: u32, data: FriendlyRDataAggregated) -> Self {
FriendlyRecord { FriendlyRecord {
ttl: ttl.into(), ttl: ttl.into(),
data, data,
} }
} }
}
#[derive(Debug)] pub fn record_type(&self) -> FriendlyRType {
pub struct FriendlyRecordSet<T> { match self.data {
ttl: i64, FriendlyRDataAggregated::Addresses(_) => FriendlyRType::Addresses,
data: Vec<T>, FriendlyRDataAggregated::MailServers(_) => FriendlyRType::MailServers,
} FriendlyRDataAggregated::NameServers(_) => FriendlyRType::NameServers,
FriendlyRDataAggregated::Service(_) => FriendlyRType::Service,
impl<T: Serialize + HasRType> Serialize for FriendlyRecordSet<T> { FriendlyRDataAggregated::Spf(_) => FriendlyRType::Spf,
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> FriendlyRDataAggregated::Texts(_) => FriendlyRType::Texts,
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<T> FriendlyRecordSet<T> {
fn new(ttl: u32) -> Self {
FriendlyRecordSet {
ttl: ttl.into(),
data: Vec::new()
} }
} }
}
#[derive(Debug, Deserialize, Serialize)] pub fn record_section(&self) -> RecordSection {
#[serde(rename_all="lowercase")] match self.data {
pub enum ConfigurationType { FriendlyRDataAggregated::Addresses(_) => RecordSection::Web,
Mail, FriendlyRDataAggregated::MailServers(_) => RecordSection::Mail,
Web, FriendlyRDataAggregated::NameServers(_) => RecordSection::Miscellaneous,
} FriendlyRDataAggregated::Service(_) => RecordSection::Services,
FriendlyRDataAggregated::Spf(_) => RecordSection::Mail,
#[derive(Debug, Serialize)] FriendlyRDataAggregated::Texts(_) => RecordSection::Miscellaneous,
pub struct MailConfiguration {
pub servers: Option<FriendlyRecordSet<MailServer>>,
pub spf: Option<FriendlyRecord<Spf>>,
}
impl MailConfiguration {
pub fn new() -> Self {
MailConfiguration {
servers: None,
spf: None
} }
} }
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct WebConfiguration { pub struct Node {
pub addresses: Option<FriendlyRecordSet<Address>>, pub name: String,
pub records: Vec<FriendlyRecord>,
} }
impl Node {
impl WebConfiguration { fn new(name: String) -> Self {
pub fn new() -> Self { Node {
WebConfiguration { name,
addresses: None, records: Vec::new(),
}
}
}
#[derive(Debug, Serialize)]
pub struct FriendlyRecordSetGroup {
owner: String,
general_records: Vec<FriendlyRecordSet<FriendlyRData>>,
#[serde(serialize_with = "as_vector")]
services: HashMap<ServiceType, FriendlyRecordSet<Service>>,
mail: Option<MailConfiguration>,
web: Option<WebConfiguration>,
}
fn as_vector<S>(services: &HashMap<ServiceType, FriendlyRecordSet<Service>>, ser: S) -> Result<S::Ok, S::Error>
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,
} }
} }
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct FriendlyRecords { pub struct FriendlyRecords {
records: Vec<FriendlyRecordSetGroup>, records: Vec<Node>,
aliases: Vec<Alias>, aliases: Vec<Alias>,
} }
@ -201,6 +145,8 @@ impl From<internal::RecordList> for FriendlyRecords {
fn from(value: internal::RecordList) -> Self { fn from(value: internal::RecordList) -> Self {
let mut records = FriendlyRecords::new(); let mut records = FriendlyRecords::new();
let mut name_mapping: HashMap<String, HashMap<FriendlyRType, FriendlyRecord>> = HashMap::new();
let mut service_mapping: HashMap<(String, ServiceType), FriendlyRecord> = HashMap::new();
for record in value.records { for record in value.records {
let internal::Record { name, ttl, rdata } = record; let internal::Record { name, ttl, rdata } = record;
@ -216,100 +162,148 @@ impl From<internal::RecordList> for FriendlyRecords {
if let FriendlyRData::Alias(alias) = rdata { if let FriendlyRData::Alias(alias) = rdata {
records.aliases.push(alias) records.aliases.push(alias)
} else { } else {
let node = name_mapping.entry(name.clone()).or_default();
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();
match rdata { match rdata {
FriendlyRData::Address(address) => { FriendlyRData::Address(address) => {
let web = current_group.web.get_or_insert_with(WebConfiguration::new); let addresses = node.entry(FriendlyRType::Addresses).or_insert_with(|| {
let addresses = web.addresses.get_or_insert_with(|| FriendlyRecordSet::new(ttl)); 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) => { FriendlyRData::MailServer(mailserver) => {
let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new); let mailservers = node.entry(FriendlyRType::MailServers).or_insert_with(|| {
let servers = mail.servers.get_or_insert_with(|| FriendlyRecordSet::new(ttl)); 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) => { FriendlyRData::Spf(spf) => {
let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new); node.insert(FriendlyRType::Spf, FriendlyRecord::new(ttl, FriendlyRDataAggregated::Spf(spf)));
mail.spf = Some(FriendlyRecord::new(ttl, spf));
}, },
FriendlyRData::Service(service) => { FriendlyRData::Service(service_single) => {
let services = current_group.services.entry(service.service_type.clone()).or_insert(FriendlyRecordSet::new(ttl)); 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?) // 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() { match nameservers.data {
current_group.general_records.push(FriendlyRecordSet::new(ttl)); FriendlyRDataAggregated::NameServers(ref mut nameservers) => nameservers.nameservers.push(nameserver),
} else { _ => unreachable!(),
let rrset = current_group.general_records.last().unwrap(); };
if rrset.data.last().unwrap().rtype() != data.rtype() { },
current_group.general_records.push(FriendlyRecordSet::new(ttl)); 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(); match texts.data {
current_rrset.data.push(data) FriendlyRDataAggregated::Texts(ref mut texts) => texts.texts.push(text),
} _ => unreachable!(),
};
},
FriendlyRData::Alias(_) => {},
} }
} }
} }
let mut nodes: HashMap<String, Node> = 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 records
} }
} }
/* --------- RDATA --------- */ /* --------- RDATA --------- */
/* --------- Address --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Address { pub struct Address {
pub address: String pub address: String
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct Addresses {
pub addresses: Vec<Address>,
}
impl HasRType for Address { /* --------- Service --------- */
fn rtype(&self) -> FriendlyRType {
FriendlyRType::Address #[derive(Debug, Deserialize, Serialize)]
} pub struct ServiceSingleTarget {
pub service_type: ServiceType,
pub service_target: ServiceTarget,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Service { pub struct Service {
pub service_type: ServiceType, pub service_type: ServiceType,
pub service_targets: Vec<ServiceTarget>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ServiceTarget {
pub port: i64, pub port: i64,
pub weight: i64, pub weight: i64,
pub priority: i64, pub priority: i64,
pub server: String, 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")] #[serde(rename_all = "lowercase", tag = "service_type")]
pub enum ServiceType { pub enum ServiceType {
Other { protocol: String, name: String }, Other { protocol: String, name: String },
} }
impl HasRType for Service { /* --------- MailServer --------- */
fn rtype(&self) -> FriendlyRType {
FriendlyRType::Service
}
}
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct MailServer { pub struct MailServer {
@ -317,36 +311,47 @@ pub struct MailServer {
pub mail_exchanger: String, pub mail_exchanger: String,
} }
impl HasRType for MailServer { #[derive(Debug, Deserialize, Serialize)]
fn rtype(&self) -> FriendlyRType { pub struct MailServers {
FriendlyRType::MailServer pub mailservers: Vec<MailServer>
}
} }
/* --------- NameServer --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct NameServer { pub struct NameServer {
pub target: String, pub target: String,
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct NameServers {
pub nameservers: Vec<NameServer>
}
/* --------- TextData --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct TextData { pub struct TextData {
pub text: String, pub text: String,
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct Texts {
pub texts: Vec<TextData>,
}
/* --------- Spf --------- */
#[derive(Debug, Deserialize, Serialize)]
pub struct Spf {
pub policy: String
}
/* --------- Alias --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Alias { pub struct Alias {
pub from: String, pub from: String,
pub target: String, pub target: String,
pub ttl: i64, pub ttl: i64,
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct Spf {
pub policy: String
}
impl HasRType for Spf {
fn rtype(&self) -> FriendlyRType {
FriendlyRType::Spf
}
}

View file

@ -147,15 +147,17 @@ impl Srv {
let service_name = labels[0]. strip_prefix('_'); let service_name = labels[0]. strip_prefix('_');
let protocol = labels[1]. strip_prefix('_'); let protocol = labels[1]. strip_prefix('_');
if let (Some(service_name), Some(protocol)) = (service_name, protocol) { 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 { service_type: friendly::ServiceType::Other {
name: service_name.into(), name: service_name.into(),
protocol: protocol.into() protocol: protocol.into()
}, },
port: self.port.into(), service_target: friendly::ServiceTarget {
weight: self.weight.into(), port: self.port.into(),
priority: self.priority.into(), weight: self.weight.into(),
server: self.server.to_string(), priority: self.priority.into(),
server: self.server.to_string()
}
}))) })))
} else { } else {
None None

View file

@ -1,6 +1,6 @@
use axum::extract::{Query, Path, State, OriginalUri}; use axum::extract::{Query, Path, State, OriginalUri};
use axum::Extension; use axum::Extension;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use serde_json::{Value, json}; use serde_json::{Value, json};
use unic_langid::LanguageIdentifier; use unic_langid::LanguageIdentifier;
@ -35,10 +35,17 @@ pub async fn get_records_page(
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct NewRecordQuery { pub struct NewRecordQuery {
subdomain: Option<String>, subdomain: Option<String>,
config: Option<friendly::ConfigurationType>, config: Option<ConfigurationType>,
rtype: Option<friendly::FriendlyRType>, rtype: Option<friendly::FriendlyRType>,
} }
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfigurationType {
Mail,
Web,
}
pub async fn get_new_record_page( pub async fn get_new_record_page(
Path(zone_name): Path<String>, Path(zone_name): Path<String>,
State(app): State<AppState>, State(app): State<AppState>,

View file

@ -1,81 +1,93 @@
{% macro rrset(rtype, ttl, data, zone, lang, service_type="") %} {% macro rrset(record, zone, lang) %}
<li class="rrset"> <li class="rrset">
<div class="rtype"> <div class="rtype">
{% if rtype == "service" %} {% if record.record_type == "service" %}
{% if service_type.service_type == "other" %} {% if record.service.service_type.service_type == "other" %}
{{ service_type.name }}/{{ service_type.protocol }} {{ record.service.service_type.name }}/{{ record.service.service_type.protocol }}
{% else %}
{{ service_type.service_type }}
{% endif %}
{% else %} {% else %}
{{ tr(msg="zone-content-record-type-" ~ rtype, attr="type-name", lang=lang) }} {{ record.srvice.service_type.service_type }}
{% endif %} {% endif %}
<div class="action"> {% else %}
<a class="button icon" href="#"> {{ tr(msg="zone-content-record-type-" ~ record.record_type, attr="type-name", lang=lang) }}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16"> {% endif %}
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/> <div class="action">
</svg> <a class="button icon" href="#">
</a> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
</div> <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
</svg>
</a>
</div> </div>
</div>
<ul> <ul>
{% for data in data %} {% if record.record_type == "addresses" %}
{% for address in record.addresses.addresses %}
<li> <li>
<div class="rdata"> <div class="rdata">
{% if rtype == "address" %}
<div class="rdata-main"> <div class="rdata-main">
<span class="pill"> <span class="pill">
{{ data.address }} {{ address.address }}
</span> </span>
</div> </div>
{% elif rtype == "mailserver" %}
<div class="rdata-main">
<span class="pill">
{{ data.mail_exchanger }}
</span>
</div>
<div class="rdata-complementary">
<span class="pill">
{{ tr(
msg="zone-content-record-type-mailserver",
attr="data-preference",
preference=data.preference,
lang=lang) }}
</span>
</div>
{% elif rtype == "nameserver" %}
<div class="rdata-main">
<span class="pill">
{{ data.target }}
</span>
</div>
{% elif rtype == "service" %}
<div class="rdata-main">
<span class="pill">
{{ data.server ~ ":" ~ data.port }}
</span>
</div>
<div class="rdata-complementary">
<span class="pill">
{{ tr(
msg="zone-content-record-type-service",
attr="data-priority",
priority=data.priority,
lang=lang) }}
</span>
<span class="pill">
{{ tr(
msg="zone-content-record-type-service",
attr="data-weight",
weight=data.weight,
lang=lang) }}
</span>
</div>
{% endif %}
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
</ul> {% elif record.record_type == "mailservers" %}
</li> {% for mailserver in record.mailservers.mailservers %}
<li>
<div class="rdata-main">
<span class="pill">
{{ mailserver.mail_exchanger }}
</span>
</div>
<div class="rdata-complementary">
<span class="pill">
{{ tr(
msg="zone-content-record-type-mailservers",
attr="data-preference",
preference=mailserver.preference,
lang=lang) }}
</span>
</div>
</li>
{% endfor %}
{% elif record.record_type == "nameservers" %}
{% for nameserver in record.nameservers.nameservers %}
<li>
<div class="rdata-main">
<span class="pill">
{{ nameserver.target }}
</span>
</div>
</li>
{% endfor %}
{% elif record.record_type == "service" %}
{% for service_target in record.service.service_targets %}
<li>
<div class="rdata-main">
<span class="pill">
{{ service_target.server ~ ":" ~ service_target.port }}
</span>
</div>
<div class="rdata-complementary">
<span class="pill">
{{ tr(
msg="zone-content-record-type-service",
attr="data-priority",
priority=service_target.priority,
lang=lang) }}
</span>
<span class="pill">
{{ tr(
msg="zone-content-record-type-service",
attr="data-weight",
weight=service_target.weight,
lang=lang) }}
</span>
</div>
</li>
{% endfor %}
{% endif %}
</ul>
</li>
{% endmacro rrset %} {% endmacro rrset %}

View file

@ -4,131 +4,13 @@
{% block main %} {% block main %}
<h1>Create a new record in zone {{ current_zone }}</h1> <h1>Create a new record in zone {{ current_zone }}</h1>
{% if not new_record_name or (new_record_name and domain_error) %} {% if not new_record_name or (new_record_name and domain_error) %}
<h2>Choose the name of the new record</h2> {% include "pages/new_record/choose_name.html" %}
<form action="" method="GET">
<label for="subdomain">Name of the new record</label>
<div class="input-group">
<input
type="text"
name="subdomain"
id="subdomain"
{% if domain_error %}aria-invalid="true"{% endif %}
aria-describedby="subdomain-help{% if domain_error %} subdomain-error{% endif %}"
>
<span>.{{ current_zone }}</span>
</div>
{% if domain_error %}
<p class="error" id="subdomain-error">{{ domain_error.description }}</p>
{% endif %}
<p id="subdomain-help">Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".</p>
<button type="submit">Next step</button>
</form>
{% elif not config and not rtype %} {% elif not config and not rtype %}
{% include "pages/new_record/choose_record.html" %}
<h2>Configure the domain {{ new_record_name }}...</h2>
<ul>
<li><a href="{{ url }}&config=web">Web site</a></li>
<li><a href="{{ url }}&config=mail">E-mails</a></li>
</ul>
<h2>...or create a new record for the domain {{ new_record_name }}</h2>
<h3>General</h3>
<ul>
<li><a href="{{ url }}&rtype=address">Address (A or AAAA)</a></li>
<li><a href="{{ url }}&rtype=alias">Alias (CNAME)</a></li>
<li><a href="{{ url }}&rtype=text">Text (TXT)</a></li>
<li><a href="{{ url }}&rtype=service">Service (SRV)</a></li>
</ul>
<h3>E-mails</h3>
<ul>sdv
<li><a href="{{ url }}&rtype=service">Mail servers (MX)</a></li>
<li><a href="{{ url }}&rtype=spf">Sender policy (SPF)</a></li>
<li><a href="{{ url }}&rtype=dkim">Cryptographic signature (DKIM)</a></li>
<li><a href="{{ url }}&rtype=dmarc">Error reporting (DMARC)</a></li>
</ul>
<h3>Security</h3>
<ul>
<li><a href="{{ url }}&rtype=dane">Domain authentication (TLSA)</a></li>
<li><a href="{{ url }}&rtype=sshfp">SSH keys fingerprint (SSHFP)</a></li>
</ul>
<h3>DNS Delegation</h3>
<ul>
<li><a href="{{ url }}&rtype=nameserver">Nameserver (NS)</a></li>
<li><a href="{{ url }}&rtype=ds">Delegation signer (DS)</a></li>
</ul>
{% else %} {% else %}
{% include "pages/new_record/configure_record.html" %}
{% if config == "web" %}
<h2>Configure a web site for the domain <strong>{{ new_record_name }}</strong></h2>
<form>
<h3>Web servers</h3>
<div class="form-input">
<label for="address">IP Address #1</label>
<input name="address" id="address" type="text">
</div>
<button type="submit">Save configuration</button>
</form>
{% elif config == "mail" %}
<h2>Configure e-mails for the domain <strong>{{ new_record_name }}</strong></h2>
<form>
<h3>Mail servers</h3>
<fieldset>
<legend>Mail server #1</legend>
<div class="form-row">
<div class="form-input">
<label for="server">Server name</label>
<input name="server" id="server" type="text">
</div>
<div class="form-input">
<label for="preference">Preference</label>
<input name="preference" id="preference" type="text">
</div>
</div>
</fieldset>
<h3>Security</h3>
<div class="form-input">
<label for="spf">Sender policy (SPF)</label>
<input name="spf" id="spf" type="text">
</div>
<div class="form-input">
<label for="dmarc">Error reporting policy (DMARC)</label>
<input name="dmarc" id="dmarc" type="text">
</div>
<fieldset>
<legend>Cryptographic signature (DKIM) #1</legend>
<div class="form-row">
<div class="form-input">
<label for="dkim-selector">Selector</label>
<input name="dkim-selector" id="dkim-selector" type="text">
</div>
<div class="form-input">
<label for="dkim-key">Signing key</label>
<textarea name="dkim-key" id="dkim-key"></textarea>
</div>
</div>
</fieldset>
<button type="submit">Save configuration</button>
</form>
{% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,19 @@
<h2>Choose the name of the new record</h2>
<form action="" method="GET">
<label for="subdomain">Name of the new record</label>
<div class="input-group">
<input
type="text"
name="subdomain"
id="subdomain"
{% if domain_error %}aria-invalid="true"{% endif %}
aria-describedby="subdomain-help{% if domain_error %} subdomain-error{% endif %}"
>
<span>.{{ current_zone }}</span>
</div>
{% if domain_error %}
<p class="error" id="subdomain-error">{{ domain_error.description }}</p>
{% endif %}
<p id="subdomain-help">Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".</p>
<button type="submit">Next step</button>
</form>

View file

@ -0,0 +1,30 @@
<h2>Configure the domain {{ new_record_name }}...</h2>
<ul>
<li><a href="{{ url }}&config=web">Web site</a></li>
<li><a href="{{ url }}&config=mail">E-mails</a></li>
</ul>
<h2>...or create a new record for the domain {{ new_record_name }}</h2>
<h3>General</h3>
<ul>
<li><a href="{{ url }}&rtype=address">Address (A or AAAA)</a></li>
<li><a href="{{ url }}&rtype=alias">Alias (CNAME)</a></li>
<li><a href="{{ url }}&rtype=text">Text (TXT)</a></li>
<li><a href="{{ url }}&rtype=service">Service (SRV)</a></li>
</ul>
<h3>E-mails</h3>
<ul>sdv
<li><a href="{{ url }}&rtype=service">Mail servers (MX)</a></li>
<li><a href="{{ url }}&rtype=spf">Sender policy (SPF)</a></li>
<li><a href="{{ url }}&rtype=dkim">Cryptographic signature (DKIM)</a></li>
<li><a href="{{ url }}&rtype=dmarc">Error reporting (DMARC)</a></li>
</ul>
<h3>Security</h3>
<ul>
<li><a href="{{ url }}&rtype=dane">Domain authentication (TLSA)</a></li>
<li><a href="{{ url }}&rtype=sshfp">SSH keys fingerprint (SSHFP)</a></li>
</ul>
<h3>DNS Delegation</h3>
<ul>
<li><a href="{{ url }}&rtype=nameserver">Nameserver (NS)</a></li>
<li><a href="{{ url }}&rtype=ds">Delegation signer (DS)</a></li>
</ul>

View file

@ -0,0 +1,68 @@
{% if config == "web" %}
<h2>Configure a web site for the domain <strong>{{ new_record_name }}</strong></h2>
<form>
<h3>Web servers</h3>
<div class="form-input">
<label for="record-0-addresses-address-0">IP Address #1</label>
<input name="records[0][addresses][addresses][0][address]" id="record-0-addresses-address-0" type="text">
</div>
<button type="submit">Save configuration</button>
</form>
{% elif config == "mail" %}
<h2>Configure e-mails for the domain <strong>{{ new_record_name }}</strong></h2>
<form>
<h3>Mail servers</h3>
<fieldset>
<legend>Mail server #1</legend>
<div class="form-row">
<div class="form-input">
<label for="server">Server name</label>
<input name="server" id="server" type="text">
</div>
<div class="form-input">
<label for="preference">Preference</label>
<input name="preference" id="preference" type="text">
</div>
</div>
</fieldset>
<h3>Security</h3>
<div class="form-input">
<label for="spf">Sender policy (SPF)</label>
<input name="spf" id="spf" type="text">
</div>
<div class="form-input">
<label for="dmarc">Error reporting policy (DMARC)</label>
<input name="dmarc" id="dmarc" type="text">
</div>
<fieldset>
<legend>Cryptographic signature (DKIM) #1</legend>
<div class="form-row">
<div class="form-input">
<label for="dkim-selector">Selector</label>
<input name="dkim-selector" id="dkim-selector" type="text">
</div>
<div class="form-input">
<label for="dkim-key">Signing key</label>
<textarea name="dkim-key" id="dkim-key"></textarea>
</div>
</div>
</fieldset>
<button type="submit">Save configuration</button>
</form>
{% endif %}

View file

@ -12,14 +12,15 @@
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
<section> <section>
<h2>{{ tr(msg="zone-content-records-header", lang=lang) }}</h2> <h2>{{ tr(msg="zone-content-records-header", lang=lang) }}</h2>
{% for group in records.records %} {% for node in records.records %}
<article class="domain"> <article class="domain">
<header> <header>
<h3 class="folder-tab">{{ group.owner }}</h3> <h3 class="folder-tab">{{ node.name }}</h3>
<span class="sep"></span> <span class="sep"></span>
<a href="{{ url }}/new?subdomain={{ group.owner | trim_end_matches(pat=current_zone) | trim_end_matches(pat=".") }}" class="button"> <a href="{{ url }}/new?subdomain={{ node.name | trim_end_matches(pat=current_zone) | trim_end_matches(pat=".") }}" class="button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
@ -27,66 +28,58 @@
{{ tr(msg="zone-content-new-record-button", lang=lang) }} {{ tr(msg="zone-content-new-record-button", lang=lang) }}
</a> </a>
</header> </header>
{% set sections = node.records | group_by(attribute="record_section") %}
<div class="records"> <div class="records">
{% if group.web %} {% if sections.web %}
{% set records = sections.web | group_by(attribute="record_type") %}
<h4>{{ tr(msg="zone-content-section-web-header", lang=lang) }}</h4> <h4>{{ tr(msg="zone-content-section-web-header", lang=lang) }}</h4>
<ul> <ul>
{% if group.web.addresses %} {% if records.addresses %}
{{ rrset::rrset( {{ rrset::rrset(
rtype=group.web.addresses.rtype, record=records.addresses.0,
ttl=group.web.addresses.ttl,
data=group.web.addresses.data,
zone=current_zone, zone=current_zone,
lang=lang) }} lang=lang) }}
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
{% if group.mail %} {% if sections.mail %}
{% set records = sections.mail | group_by(attribute="record_type") %}
<h4>{{ tr(msg="zone-content-section-mail-header", lang=lang) }}</h4> <h4>{{ tr(msg="zone-content-section-mail-header", lang=lang) }}</h4>
<ul> <ul>
{% if group.mail.servers %} {% if records.mailservers %}
{{ rrset::rrset( {{ rrset::rrset(
rtype=group.mail.servers.rtype, record=records.mailservers.0,
ttl=group.mail.servers.ttl,
data=group.mail.servers.data,
zone=current_zone, zone=current_zone,
lang=lang) }} lang=lang) }}
{% endif %} {% endif %}
{% if group.mail.spf %} {% if records.spf %}
{{ rrset::rrset( {{ rrset::rrset(
rtype=group.mail.spf.rtype, record=records.spf.0,
ttl=group.mail.spf.ttl,
data=group.mail.spf.data,
zone=current_zone, zone=current_zone,
lang=lang) }} lang=lang) }}
{% endif %} {% endif %}
</ul> </ul>
{% endif %} {% endif %}
{% if group.services %} {% if sections.services %}
<h4>{{ tr(msg="zone-content-section-services-header", lang=lang) }}</h4> <h4>{{ tr(msg="zone-content-section-services-header", lang=lang) }}</h4>
<ul> <ul>
{% for service in group.services %} {% for service in sections.services %}
{{ rrset::rrset( {{ rrset::rrset(
rtype=service[1].rtype, record=service,
ttl=service[1].ttl,
data=service[1].data,
zone=current_zone, zone=current_zone,
lang=lang, lang=lang) }}
service_type=service[0]) }}
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
{% if group.general_records %} {% if sections.miscellaneous %}
<h4>{{ tr(msg="zone-content-section-general-header", lang=lang) }}</h4> <h4>{{ tr(msg="zone-content-section-general-header", lang=lang) }}</h4>
<ul> <ul>
{% for rrset in group.general_records %} {% for record in sections.miscellaneous %}
{{ rrset::rrset( {{ rrset::rrset(
rtype=rrset.rtype, record=record,
ttl=rrset.ttl,
data=rrset.data,
zone=current_zone, zone=current_zone,
lang=lang) }} lang=lang) }}
{% endfor %} {% endfor %}