add create zone page

gg#	modified:   src/models/name.rs
main
Hannaeko 2023-02-22 16:28:12 +01:00
parent 1cd58f6ff7
commit 3c5de4cab6
14 changed files with 265 additions and 78 deletions

View File

@ -1,10 +1,5 @@
main {
margin-top: 20vh;
}
form {
display: flex;
flex-direction: column;
max-width: 25rem;
margin: auto;
flex-grow: 1;
}

View File

@ -7,6 +7,8 @@ body {
main {
flex-grow: 1;
display: flex;
padding: 1rem;
}
input {
@ -14,14 +16,31 @@ input {
font-size: 1rem;
}
:root {
--color-primary: #712da0;
--color-hightlight-1: #ffbac6;
--color-hightlight-2: #f560f5;
--color-contrast: white;
}
p.feedback {
padding: .35rem;
margin: 0;
}
p.feedback.error {
background: #fddede;
color: #710000;
}
input[type="submit"] {
margin-top: 2rem;
background: #712da0;
color: white;
border-left: 5px solid #ffbac6;
border-top: 5px solid #ffbac6;
border-right: 5px solid #f560f5;
border-bottom: 5px solid #f560f5;
background: var(--color-primary);
color: var(--color-contrast);
border-left: 5px solid var(--color-hightlight-1);
border-top: 5px solid var(--color-hightlight-1);
border-right: 5px solid var(--color-hightlight-2);
border-bottom: 5px solid var(--color-hightlight-2);
}
input[type="submit"]:hover {
@ -36,6 +55,38 @@ form label {
margin-top: .75em;
}
form {
display: flex;
flex-direction: column;
}
nav {
background: var(--color-primary);
max-width: 10vw;
display: flex;
flex: 1;
padding: 1rem;
border-right: 5px solid var(--color-hightlight-1);
}
nav a {
color: var(--color-contrast);
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
}
nav ul li {
margin-top: .35rem;
}
nav ul ul {
margin-left: 1rem;
}
zone-content table {
border-collapse: collapse;
width: 100%;

View File

@ -104,6 +104,8 @@ impl NomiloCommand for RunServerCommand {
ui::post_login_page,
ui::get_zones_page,
ui::get_zone_records_page,
ui::get_create_zone_page,
ui::post_create_zone_page,
])
.mount("/", static_files)
.launch().await;

View File

@ -2,6 +2,7 @@ use rocket::http::{Cookie, SameSite, CookieJar};
use rocket::State;
use crate::config::Config;
use crate::dns::ZoneConnector;
use crate::DbConn;
use crate::models;
@ -39,3 +40,35 @@ pub async fn do_login(
Ok(session)
}
pub async fn create_zone(
conn: &DbConn,
mut dns_api: Box<dyn ZoneConnector>,
user_info: models::UserInfo,
zone_request: models::CreateZoneRequest,
) -> Result<models::Zone, models::ErrorResponse> {
user_info.check_admin()?;
dns_api.zone_exists(zone_request.name.clone(), models::DNSClass::IN.into()).await?;
let zone = conn.run(move |c| {
models::Zone::create_zone(c, zone_request)
}).await?;
Ok(zone)
}
pub async fn get_zones(
conn: &DbConn,
user_info: models::UserInfo,
) -> Result<Vec<models::Zone>, models::ErrorResponse> {
let zones = conn.run(move |c| {
if user_info.is_admin() {
models::Zone::get_all(c)
} else {
user_info.get_zones(c)
}
}).await?;
Ok(zones)
}

View File

@ -31,8 +31,10 @@ impl DerefMut for DnsClient {
impl DnsClient {
pub async fn from_config(dns_config: &DnsClientConfig) -> Result<Self, ProtoError> {
info!("Creating DNS client for {}", dns_config.server);
let (stream, handle) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(dns_config.server);
let signer = if let Some(tsig_config) = dns_config.tsig.as_ref() {
info!("Client configured with TSIG authentication");
Some(Arc::new(TSigner::new(
tsig_config.key.clone(),
tsig_config.algorithm.clone(),
@ -40,6 +42,7 @@ impl DnsClient {
60,
)?.into()))
} else {
info!("Client configured without authentication");
None
};
@ -77,4 +80,3 @@ where
)
}
}

View File

@ -250,6 +250,7 @@ impl ZoneConnector for DnsConnectorClient {
async fn zone_exists(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<()>
{
let response = {
info!("Querying SOA for name {}", zone);
let query = self.client.query(zone.clone(), class, RecordType::SOA);
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
Err(e) => return Err(e),

View File

@ -2,6 +2,7 @@ use std::ops::Deref;
use rocket::request::FromParam;
use rocket::form::{self, FromFormField, ValueField};
use serde::{Deserialize, Serialize, Deserializer, Serializer};
use trust_dns_proto::error::ProtoError;
@ -48,6 +49,13 @@ impl SerdeName {
}
}
fn parse_absolute_name(name: &str) -> Result<AbsoluteName, ProtoError> {
let mut name = Name::from_utf8(name)?;
if !name.is_fqdn() {
name.set_fqdn(true);
}
Ok(AbsoluteName(SerdeName(name)))
}
#[derive(Debug, Deserialize)]
pub struct AbsoluteName(SerdeName);
@ -56,14 +64,22 @@ impl<'r> FromParam<'r> for AbsoluteName {
type Error = ProtoError;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
let mut name = Name::from_utf8(&param)?;
if !name.is_fqdn() {
name.set_fqdn(true);
}
Ok(AbsoluteName(SerdeName(name)))
let name = parse_absolute_name(param)?;
Ok(name)
}
}
#[async_trait]
impl<'v> FromFormField<'v> for AbsoluteName {
fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> {
let name = parse_absolute_name(field.value)
.map_err(|_| form::Error::validation("Invalid name"))?;
Ok(name)
}
}
impl Deref for AbsoluteName {
type Target = Name;
fn deref(&self) -> &Self::Target {

View File

@ -60,7 +60,7 @@ pub struct CreateUserRequest {
pub role: Option<Role>
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct UserInfo {
pub id: String,
pub role: Role,

View File

@ -24,7 +24,7 @@ pub struct AddZoneMemberRequest {
pub id: String,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, FromForm)]
pub struct CreateZoneRequest {
pub name: AbsoluteName,
}

View File

@ -6,6 +6,7 @@ use crate::DbConn;
use crate::dns::{RecordConnector, ZoneConnector};
use crate::models;
use crate::models::{ParseRecordList};
use crate::controllers;
#[get("/zones/<zone>/records")]
@ -130,33 +131,27 @@ pub async fn get_zones(
) -> Result<Json<Vec<models::Zone>>, models::ErrorResponse> {
let user_info = user_info?;
let zones = conn.run(move |c| {
if user_info.is_admin() {
models::Zone::get_all(c)
} else {
user_info.get_zones(c)
}
}).await?;
Ok(Json(zones))
controllers::get_zones(
&conn,
user_info
).await.map(|zones| Json(zones))
}
#[post("/zones", data = "<zone_request>")]
pub async fn create_zone(
conn: DbConn,
mut dns_api: Box<dyn ZoneConnector>,
dns_api: Box<dyn ZoneConnector>,
user_info: Result<models::UserInfo, models::ErrorResponse>,
zone_request: Json<models::CreateZoneRequest>,
) -> Result<Json<models::Zone>, models::ErrorResponse> {
user_info?.check_admin()?;
let user_info = user_info?;
dns_api.zone_exists(zone_request.name.clone(), models::DNSClass::IN.into()).await?;
let zone = conn.run(move |c| {
models::Zone::create_zone(c, zone_request.into_inner())
}).await?;
Ok(Json(zone))
controllers::create_zone(
&conn,
dns_api,
user_info,
zone_request.into_inner()
).await.map(|zone| Json(zone))
}

View File

@ -2,10 +2,13 @@ use serde_json::{Value, json};
use serde::Serialize;
use rocket::http::{Status};
use rocket::http::uri::Origin;
use rocket::form::Form;
use crate::template::Template;
use crate::models;
use crate::controllers;
use crate::DbConn;
use crate::dns::ZoneConnector;
#[derive(Serialize)]
@ -44,14 +47,10 @@ pub async fn get_zone_records_page(user_info: models::UserInfo, zone: models::Ab
#[get("/zones")]
pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, Status> {
let zones = conn.run(move |c| {
if user_info.is_admin() {
models::Zone::get_all(c)
} else {
user_info.get_zones(c)
}
}).await.map_err(|e| models::ErrorResponse::from(e).status)?;
let zones = controllers::get_zones(
&conn,
user_info
).await.map_err(|e| e.status)?;
Ok(Template::new(
"pages/zones.html",
@ -62,3 +61,68 @@ pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &
})
))
}
#[get("/zones/new")]
pub async fn get_create_zone_page(
conn: DbConn,
user_info: models::UserInfo,
origin: &Origin<'_>
) -> Result<Template<'static, Value>, Status> {
user_info
.check_admin()
.map_err(|e| models::ErrorResponse::from(e).status)?;
let zones = controllers::get_zones(
&conn,
user_info
).await.map_err(|e| e.status)?;
Ok(Template::new(
"pages/zones/new.html",
json!({
"zone": None::<models::Zone>,
"zones": zones,
"error": None::<String>,
"nav_page": origin.clone().into_normalized().path().as_str(),
"nav_sections": vec!["zones", "_new-zone"],
})
))
}
#[post("/zones/new", data = "<zone_request>")]
pub async fn post_create_zone_page(
conn: DbConn,
dns_api: Box<dyn ZoneConnector>,
user_info: models::UserInfo,
zone_request: Form<models::CreateZoneRequest>,
origin: &Origin<'_>
) -> Result<Template<'static, Value>, Status> {
user_info
.check_admin()
.map_err(|e| models::ErrorResponse::from(e).status)?;
let zone = controllers::create_zone(
&conn,
dns_api,
user_info.clone(),
zone_request.into_inner()
).await.map_err(|e| e.status)?;
let zones = controllers::get_zones(
&conn,
user_info
).await.map_err(|e| e.status)?;
Ok(Template::new(
"pages/zones/new.html",
json!({
"zone": Some(zone),
"zones": zones,
"error": None::<String>,
"nav_page": origin.clone().into_normalized().path().as_str(),
"nav_sections": vec!["zones", "_new-zone"],
})
))
}

View File

@ -2,33 +2,43 @@
{% import "macros.html" as macros %}
{% block content %}
<nav aria-label="Principal">
<ul>
<li><a href="/profil">Mon profile</a></li>
<li>
{{ macros::nav_link(
content="Mes zones",
href="/zones",
current_page=nav_page,
section="zones",
current_sections=nav_sections,
) }}
<ul>
{% for zone in zones %}
<li>
{{ macros::nav_link(
content=zone.name,
href="/zone/" ~ zone.name,
current_page=nav_page,
section=zone.name,
current_sections=nav_sections,
) }}
{% endfor %}
</ul>
</li>
</ul>
</nav>
<nav aria-label="Principal">
<ul>
<li><a href="/profil">Mon profile</a></li>
<li>
{{ macros::nav_link(
content="Mes zones",
href="/zones",
current_page=nav_page,
section="zones",
current_sections=nav_sections,
) }}
<ul>
{% for zone in zones %}
<li>
{{ macros::nav_link(
content=zone.name,
href="/zone/" ~ zone.name,
current_page=nav_page,
section=zone.name,
current_sections=nav_sections,
) }}
</li>
{% endfor %}
<li>
{{ macros::nav_link(
content="Ajouter une zone",
href="/zones/new",
current_page=nav_page,
section="_new-zone",
current_sections=nav_sections,
) }}
</li>
</ul>
</li>
</ul>
</nav>
<main>
{% block main %}{% endblock main %}
{% block main %}{% endblock main %}
</main>
{% endblock content %}

View File

@ -7,12 +7,13 @@
{% block content %}
<main>
{% if error %}
<p>
{{ error }}
</p>
{% endif %}
<form method="POST" action="/login">
{% if error %}
<p class="feedback error" role="alert">
{{ error }}
</p>
{% endif %}
<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email">
<label for="password">Mot de passe</label>

View File

@ -0,0 +1,17 @@
{% extends "bases/app.html" %}
{% block title %}Créer une zone ⋅ {% endblock title %}
{% block main %}
<form method="POST" action="/zones/new">
{% if error %}
<p class="feedback error" role="alert">
{{ error }}
</p>
{% endif %}
<label for="zone_name">Nom de la zone</label>
<input type="text" id="zone_name" name="name">
<input type="submit" value="Créer la zone">
</form>
{% endblock main %}