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 { form {
display: flex;
flex-direction: column;
max-width: 25rem; max-width: 25rem;
margin: auto; margin: auto;
flex-grow: 1;
} }

View File

@ -7,6 +7,8 @@ body {
main { main {
flex-grow: 1; flex-grow: 1;
display: flex;
padding: 1rem;
} }
input { input {
@ -14,14 +16,31 @@ input {
font-size: 1rem; 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"] { input[type="submit"] {
margin-top: 2rem; margin-top: 2rem;
background: #712da0; background: var(--color-primary);
color: white; color: var(--color-contrast);
border-left: 5px solid #ffbac6; border-left: 5px solid var(--color-hightlight-1);
border-top: 5px solid #ffbac6; border-top: 5px solid var(--color-hightlight-1);
border-right: 5px solid #f560f5; border-right: 5px solid var(--color-hightlight-2);
border-bottom: 5px solid #f560f5; border-bottom: 5px solid var(--color-hightlight-2);
} }
input[type="submit"]:hover { input[type="submit"]:hover {
@ -36,6 +55,38 @@ form label {
margin-top: .75em; 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 { zone-content table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;

View File

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

View File

@ -2,6 +2,7 @@ use rocket::http::{Cookie, SameSite, CookieJar};
use rocket::State; use rocket::State;
use crate::config::Config; use crate::config::Config;
use crate::dns::ZoneConnector;
use crate::DbConn; use crate::DbConn;
use crate::models; use crate::models;
@ -39,3 +40,35 @@ pub async fn do_login(
Ok(session) 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 { impl DnsClient {
pub async fn from_config(dns_config: &DnsClientConfig) -> Result<Self, ProtoError> { 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 (stream, handle) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(dns_config.server);
let signer = if let Some(tsig_config) = dns_config.tsig.as_ref() { let signer = if let Some(tsig_config) = dns_config.tsig.as_ref() {
info!("Client configured with TSIG authentication");
Some(Arc::new(TSigner::new( Some(Arc::new(TSigner::new(
tsig_config.key.clone(), tsig_config.key.clone(),
tsig_config.algorithm.clone(), tsig_config.algorithm.clone(),
@ -40,6 +42,7 @@ impl DnsClient {
60, 60,
)?.into())) )?.into()))
} else { } else {
info!("Client configured without authentication");
None 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<()> async fn zone_exists(&mut self, zone: Name, class: DNSClass) -> ConnectorResult<()>
{ {
let response = { let response = {
info!("Querying SOA for name {}", zone);
let query = self.client.query(zone.clone(), class, RecordType::SOA); let query = self.client.query(zone.clone(), class, RecordType::SOA);
match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) { match query.await.map_err(|e| Box::new(DnsConnectorError::ClientError(e))) {
Err(e) => return Err(e), Err(e) => return Err(e),

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,13 @@ use serde_json::{Value, json};
use serde::Serialize; use serde::Serialize;
use rocket::http::{Status}; use rocket::http::{Status};
use rocket::http::uri::Origin; use rocket::http::uri::Origin;
use rocket::form::Form;
use crate::template::Template; use crate::template::Template;
use crate::models; use crate::models;
use crate::controllers;
use crate::DbConn; use crate::DbConn;
use crate::dns::ZoneConnector;
#[derive(Serialize)] #[derive(Serialize)]
@ -44,14 +47,10 @@ pub async fn get_zone_records_page(user_info: models::UserInfo, zone: models::Ab
#[get("/zones")] #[get("/zones")]
pub async fn get_zones_page(user_info: models::UserInfo, conn: DbConn, origin: &Origin<'_>) -> Result<Template<'static, Value>, Status> { 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| { let zones = controllers::get_zones(
if user_info.is_admin() { &conn,
models::Zone::get_all(c) user_info
} else { ).await.map_err(|e| e.status)?;
user_info.get_zones(c)
}
}).await.map_err(|e| models::ErrorResponse::from(e).status)?;
Ok(Template::new( Ok(Template::new(
"pages/zones.html", "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

@ -23,7 +23,17 @@
section=zone.name, section=zone.name,
current_sections=nav_sections, current_sections=nav_sections,
) }} ) }}
</li>
{% endfor %} {% 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> </ul>
</li> </li>
</ul> </ul>

View File

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