diff --git a/public/styles/login.css b/public/styles/login.css index 8624e9c..fa12de6 100644 --- a/public/styles/login.css +++ b/public/styles/login.css @@ -1,10 +1,5 @@ -main { - margin-top: 20vh; -} - form { - display: flex; - flex-direction: column; max-width: 25rem; margin: auto; + flex-grow: 1; } diff --git a/public/styles/main.css b/public/styles/main.css index 2ee6be4..80af548 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -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%; diff --git a/src/cli/server.rs b/src/cli/server.rs index 8d55109..0f13fa3 100644 --- a/src/cli/server.rs +++ b/src/cli/server.rs @@ -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; diff --git a/src/controllers.rs b/src/controllers.rs index 6635298..1a27199 100644 --- a/src/controllers.rs +++ b/src/controllers.rs @@ -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, + user_info: models::UserInfo, + zone_request: models::CreateZoneRequest, +) -> Result { + 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, 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) +} diff --git a/src/dns/client.rs b/src/dns/client.rs index d68df3b..bb1ca07 100644 --- a/src/dns/client.rs +++ b/src/dns/client.rs @@ -31,8 +31,10 @@ impl DerefMut for DnsClient { impl DnsClient { pub async fn from_config(dns_config: &DnsClientConfig) -> Result { + info!("Creating DNS client for {}", dns_config.server); let (stream, handle) = TcpClientStream::>::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 ) } } - diff --git a/src/dns/dns_connector.rs b/src/dns/dns_connector.rs index 2b549b4..025eff6 100644 --- a/src/dns/dns_connector.rs +++ b/src/dns/dns_connector.rs @@ -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), diff --git a/src/models/name.rs b/src/models/name.rs index 1ebc3a7..c768b28 100644 --- a/src/models/name.rs +++ b/src/models/name.rs @@ -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 { + 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 { - let mut name = Name::from_utf8(¶m)?; - 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 { diff --git a/src/models/user.rs b/src/models/user.rs index 277f416..a7b9219 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -60,7 +60,7 @@ pub struct CreateUserRequest { pub role: Option } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UserInfo { pub id: String, pub role: Role, diff --git a/src/models/zone.rs b/src/models/zone.rs index 4285352..cdad91b 100644 --- a/src/models/zone.rs +++ b/src/models/zone.rs @@ -24,7 +24,7 @@ pub struct AddZoneMemberRequest { pub id: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, FromForm)] pub struct CreateZoneRequest { pub name: AbsoluteName, } diff --git a/src/routes/api/zones.rs b/src/routes/api/zones.rs index dbec883..f2cdd73 100644 --- a/src/routes/api/zones.rs +++ b/src/routes/api/zones.rs @@ -6,6 +6,7 @@ use crate::DbConn; use crate::dns::{RecordConnector, ZoneConnector}; use crate::models; use crate::models::{ParseRecordList}; +use crate::controllers; #[get("/zones//records")] @@ -130,33 +131,27 @@ pub async fn get_zones( ) -> Result>, 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 = "")] pub async fn create_zone( conn: DbConn, - mut dns_api: Box, + dns_api: Box, user_info: Result, zone_request: Json, ) -> Result, 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)) } diff --git a/src/routes/ui/zones.rs b/src/routes/ui/zones.rs index ff61bcf..dc3ae4d 100644 --- a/src/routes/ui/zones.rs +++ b/src/routes/ui/zones.rs @@ -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, 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, 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::, + "zones": zones, + "error": None::, + "nav_page": origin.clone().into_normalized().path().as_str(), + "nav_sections": vec!["zones", "_new-zone"], + }) + )) +} + +#[post("/zones/new", data = "")] +pub async fn post_create_zone_page( + conn: DbConn, + dns_api: Box, + user_info: models::UserInfo, + zone_request: Form, + origin: &Origin<'_> +) -> Result, 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::, + "nav_page": origin.clone().into_normalized().path().as_str(), + "nav_sections": vec!["zones", "_new-zone"], + }) + )) +} diff --git a/templates/bases/app.html b/templates/bases/app.html index 6a69255..c9990dd 100644 --- a/templates/bases/app.html +++ b/templates/bases/app.html @@ -2,33 +2,43 @@ {% import "macros.html" as macros %} {% block content %} - +
- {% block main %}{% endblock main %} + {% block main %}{% endblock main %}
{% endblock content %} diff --git a/templates/pages/login.html b/templates/pages/login.html index de732da..93b5511 100644 --- a/templates/pages/login.html +++ b/templates/pages/login.html @@ -7,12 +7,13 @@ {% block content %}
- {% if error %} -

- {{ error }} -

- {% endif %}
+ {% if error %} + + {% endif %} + diff --git a/templates/pages/zones/new.html b/templates/pages/zones/new.html new file mode 100644 index 0000000..2c59644 --- /dev/null +++ b/templates/pages/zones/new.html @@ -0,0 +1,17 @@ +{% extends "bases/app.html" %} + +{% block title %}Créer une zone ⋅ {% endblock title %} + +{% block main %} + + {% if error %} + + {% endif %} + + + + +
+{% endblock main %}