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-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 }

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;
#[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<T> {
pub struct FriendlyRecord {
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>
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<T> FriendlyRecord<T> {
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<T> {
ttl: i64,
data: Vec<T>,
}
impl<T: Serialize + HasRType> Serialize for FriendlyRecordSet<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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()
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<FriendlyRecordSet<MailServer>>,
pub spf: Option<FriendlyRecord<Spf>>,
}
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<FriendlyRecordSet<Address>>,
pub struct Node {
pub name: String,
pub records: Vec<FriendlyRecord>,
}
impl WebConfiguration {
pub fn new() -> Self {
WebConfiguration {
addresses: None,
}
}
}
#[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,
impl Node {
fn new(name: String) -> Self {
Node {
name,
records: Vec::new(),
}
}
}
#[derive(Debug, Serialize)]
pub struct FriendlyRecords {
records: Vec<FriendlyRecordSetGroup>,
records: Vec<Node>,
aliases: Vec<Alias>,
}
@ -201,6 +145,8 @@ impl From<internal::RecordList> for FriendlyRecords {
fn from(value: internal::RecordList) -> Self {
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 {
let internal::Record { name, ttl, rdata } = record;
@ -216,100 +162,148 @@ impl From<internal::RecordList> 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<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
}
}
/* --------- RDATA --------- */
/* --------- Address --------- */
#[derive(Debug, Deserialize, Serialize)]
pub struct Address {
pub address: String
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Addresses {
pub addresses: Vec<Address>,
}
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<ServiceTarget>,
}
#[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<MailServer>
}
/* --------- NameServer --------- */
#[derive(Debug, Deserialize, Serialize)]
pub struct NameServer {
pub target: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct NameServers {
pub nameservers: Vec<NameServer>
}
/* --------- TextData --------- */
#[derive(Debug, Deserialize, Serialize)]
pub struct TextData {
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)]
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
}
}

View file

@ -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

View file

@ -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<String>,
config: Option<friendly::ConfigurationType>,
config: Option<ConfigurationType>,
rtype: Option<friendly::FriendlyRType>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfigurationType {
Mail,
Web,
}
pub async fn get_new_record_page(
Path(zone_name): Path<String>,
State(app): State<AppState>,

View file

@ -1,81 +1,93 @@
{% macro rrset(rtype, ttl, data, zone, lang, service_type="") %}
<li class="rrset">
<div class="rtype">
{% 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) %}
<li class="rrset">
<div class="rtype">
{% 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 %}
<div class="action">
<a class="button icon" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
<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>
{% else %}
{{ tr(msg="zone-content-record-type-" ~ record.record_type, attr="type-name", lang=lang) }}
{% endif %}
<div class="action">
<a class="button icon" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
<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>
<ul>
{% for data in data %}
<ul>
{% if record.record_type == "addresses" %}
{% for address in record.addresses.addresses %}
<li>
<div class="rdata">
{% if rtype == "address" %}
<div class="rdata-main">
<span class="pill">
{{ data.address }}
{{ address.address }}
</span>
</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>
</li>
{% endfor %}
</ul>
</li>
{% elif record.record_type == "mailservers" %}
{% 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 %}

View file

@ -4,131 +4,13 @@
{% block main %}
<h1>Create a new record in zone {{ current_zone }}</h1>
{% if not new_record_name or (new_record_name and domain_error) %}
<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>
{% include "pages/new_record/choose_name.html" %}
{% elif not config and not rtype %}
<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>
{% include "pages/new_record/choose_record.html" %}
{% else %}
{% 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 %}
{% include "pages/new_record/configure_record.html" %}
{% endif %}
{% 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>
</defs>
</svg>
<section>
<h2>{{ tr(msg="zone-content-records-header", lang=lang) }}</h2>
{% for group in records.records %}
{% for node in records.records %}
<article class="domain">
<header>
<h3 class="folder-tab">{{ group.owner }}</h3>
<h3 class="folder-tab">{{ node.name }}</h3>
<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">
<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"/>
@ -27,66 +28,58 @@
{{ tr(msg="zone-content-new-record-button", lang=lang) }}
</a>
</header>
{% set sections = node.records | group_by(attribute="record_section") %}
<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>
<ul>
{% 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 %}
</ul>
{% 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>
<ul>
{% 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 %}
</ul>
{% endif %}
{% if group.services %}
{% if sections.services %}
<h4>{{ tr(msg="zone-content-section-services-header", lang=lang) }}</h4>
<ul>
{% 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 %}
</ul>
{% endif %}
{% if group.general_records %}
{% if sections.miscellaneous %}
<h4>{{ tr(msg="zone-content-section-general-header", lang=lang) }}</h4>
<ul>
{% 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 %}