From 4b616447715b8129ae322341959e1c2bfabbd10e Mon Sep 17 00:00:00 2001 From: Cara Salter Date: Wed, 20 Jul 2022 10:31:01 -0400 Subject: emails also subnet storage --- Cargo.lock | 260 +++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/config.rs | 13 +++ src/errors.rs | 10 ++ src/handlers/auth.rs | 12 +++ src/handlers/mod.rs | 10 +- src/handlers/nets.rs | 50 +++++++++ src/main.rs | 15 ++- src/models.rs | 20 +++- src/utils.rs | 51 +++++++++ templates/index.rs.html | 63 ++++++++++- templates/new_net.rs.html | 21 ++++ 12 files changed, 517 insertions(+), 10 deletions(-) create mode 100644 src/handlers/nets.rs create mode 100644 src/utils.rs create mode 100644 templates/new_net.rs.html diff --git a/Cargo.lock b/Cargo.lock index 6be56e3..9010c78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -439,6 +439,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.2" @@ -574,6 +590,22 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +[[package]] +name = "email-encoding" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8684b7c9cb4857dfa1e5b9629ef584ba618c9b93bae60f58cb23f4f271d0468e" + [[package]] name = "event-listener" version = "2.5.2" @@ -605,6 +637,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -642,6 +689,12 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + [[package]] name = "futures-sink" version = "0.3.21" @@ -661,10 +714,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-core", + "futures-io", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -793,6 +849,17 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.8" @@ -908,6 +975,15 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "itertools" version = "0.10.3" @@ -938,6 +1014,33 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lettre" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" +dependencies = [ + "async-trait", + "base64", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2", + "tokio", + "tokio-native-tls", + "tracing", +] + [[package]] name = "libc" version = "0.2.126" @@ -963,6 +1066,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -1048,6 +1157,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nccd" version = "0.1.0" @@ -1060,6 +1187,8 @@ dependencies = [ "chrono", "color-eyre", "hyper", + "ipnetwork 0.20.0", + "lettre", "ructe", "serde", "sqlx", @@ -1175,6 +1304,51 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "owo-colors" version = "3.4.0" @@ -1273,6 +1447,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "polyval" version = "0.5.3" @@ -1309,6 +1489,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" + [[package]] name = "rand" version = "0.8.5" @@ -1383,6 +1569,15 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -1461,6 +1656,16 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1477,6 +1682,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.139" @@ -1649,7 +1877,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap", - "ipnetwork", + "ipnetwork 0.19.0", "itoa", "libc", "log", @@ -1739,6 +1967,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.31" @@ -1844,6 +2086,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.23.4" @@ -2114,6 +2366,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index b055fd1..60187f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ chrono = "0.4.19" ulid = "0.6.0" async-session = "3.0.0" bcrypt = "0.13.0" +ipnetwork = "0.20.0" +lettre = { version = "0.10.1", features = ["tokio1", "tracing", "tokio1-native-tls"] } [dependencies.sqlx] version = "0.6" diff --git a/src/config.rs b/src/config.rs index 4ceba75..c153085 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,6 +9,7 @@ use std::str; pub struct Config { pub server: ServerConfig, pub database: DbConfig, + pub email: EmailConfig, } #[derive(Deserialize)] @@ -23,6 +24,18 @@ pub struct DbConfig { pub max_connections: u32, } +#[derive(Deserialize)] +pub struct EmailConfig { + pub smtp_server: String, + pub smtp_port: u16, + pub smtp_tls: bool, + pub smtp_starttls: bool, + pub smtp_username: Option, + pub smtp_password: Option, + pub email_from: String, + pub email_helo: String, +} + impl Config { pub fn init(path: String) -> Result { if let Ok(c) = fs::read(path) { diff --git a/src/errors.rs b/src/errors.rs index 23ad8fa..dcc0ae8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -19,6 +19,10 @@ pub enum ServiceError { Sql(#[from] sqlx::Error), #[error("Bcrypt: {0}")] Bcrypt(#[from] bcrypt::BcryptError), + #[error("Parse Error: {0}")] + Parse(String), + #[error("Email: {0}")] + Email(String), } pub type StringResult = Result; @@ -37,3 +41,9 @@ impl IntoResponse for ServiceError { Response::builder().status(status).body(body).unwrap() } } + +impl From for ServiceError { + fn from(e: ipnetwork::IpNetworkError) -> Self { + Self::Parse(e.to_string()) + } +} diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index c00fb8d..ab72bc8 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -82,6 +82,18 @@ pub async fn register_post(Form(reg): Form, state: Extension Result<(PrivateCookieJar, Redirect), ServiceError> { + if let Some(id) = jar.get("user-id") { + debug!("Found user {}", id); + + let updated_jar = jar.remove(id); + + Ok((updated_jar, Redirect::to("/dash/auth/login"))) + } else { + Ok((jar, Redirect::to("/"))) + } +} + #[instrument] pub async fn get_user_or_403(jar: PrivateCookieJar, conn: &mut PoolConnection) -> Result { debug!("Starting middleware get_user_or_403"); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index b83d83c..24db540 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,11 +1,13 @@ -use axum::{Router, routing::get}; +use axum::{Router, routing::{get, post}}; pub mod auth; +mod nets; pub async fn gen_routers() -> Router { Router::new() .nest("/auth", auth_routes().await) + .nest("/nets", net_routes().await) } async fn auth_routes() -> Router { @@ -13,4 +15,10 @@ async fn auth_routes() -> Router { Router::new() .route("/login", get(auth::login).post(auth::login_post)) .route("/register", get(auth::register).post(auth::register_post)) + .route("/logout", post(auth::logout_post)) +} + +async fn net_routes() -> Router { + Router::new() + .route("/new", get(nets::new).post(nets::new_post)) } diff --git a/src/handlers/nets.rs b/src/handlers/nets.rs new file mode 100644 index 0000000..6010787 --- /dev/null +++ b/src/handlers/nets.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use axum::{Extension, Form}; +use axum::response::{Html, IntoResponse, Redirect}; +use axum_extra::extract::PrivateCookieJar; +use sqlx::query; +use sqlx::types::ipnetwork::IpNetwork; +use serde::Deserialize; + +use crate::State; + +use crate::errors::{HtmlResult, ServiceError}; + +use super::auth::get_user_or_403; + +#[derive(Deserialize)] +pub struct NewNetForm { + pub subnet: String, + pub description: String, +} + +pub async fn new(jar: PrivateCookieJar, state: Extension>) -> Result>, ServiceError> { + let mut conn = state.conn.acquire().await?; + + let _ = get_user_or_403(jar, &mut conn).await?; + + let mut buf = Vec::new(); + crate::templates::new_net_html(&mut buf).unwrap(); + + Ok(Html(buf)) +} + +pub async fn new_post(Form(new): Form, jar: PrivateCookieJar, state: Extension>) -> Result { + let mut conn = state.conn.acquire().await?; + + let _ = get_user_or_403(jar, &mut conn).await?; + + let id = ulid::Ulid::new(); + + let cidr: IpNetwork = match new.subnet.parse() { + Ok(c) => c, + Err(e) => { + return Err(ServiceError::Parse(e.to_string())); + } + }; + + query("INSERT INTO networks (subnet, description, id) VALUES ($1, $2, $3)").bind(cidr).bind(new.description).bind(id.to_string()).execute(&mut conn).await?; + + Ok(Redirect::to("/")) +} diff --git a/src/main.rs b/src/main.rs index ab03fb3..6ebe145 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod config; mod errors; mod handlers; mod models; +mod utils; use std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; @@ -13,6 +14,8 @@ use axum_extra::extract::{PrivateCookieJar, SignedCookieJar}; use axum_extra::extract::cookie::Key; use errors::{StringResult, HtmlResult}; use hyper::StatusCode; +use models::{Peer, Network}; +use sqlx::query_as; use sqlx::{PgPool, postgres::PgPoolOptions}; use tower::ServiceBuilder; use tower_http::trace::TraceLayer; @@ -106,8 +109,12 @@ async fn index(state: Extension>, jar: PrivateCookieJar) -> HtmlResul Ok(u) => Some(u), Err(_) => None, }; + + let peers: Vec = query_as("SELECT * FROM peers").fetch_all(&mut conn).await?; + let nets: Vec = query_as("SELECT * FROM networks").fetch_all(&mut conn).await?; + let mut buf = Vec::new(); - crate::templates::index_html(&mut buf, user).unwrap(); + crate::templates::index_html(&mut buf, user, peers, nets).unwrap(); match String::from_utf8(buf) { Ok(s) => Ok(Html(s)), @@ -115,6 +122,12 @@ async fn index(state: Extension>, jar: PrivateCookieJar) -> HtmlResul } } +async fn test_email() -> Result<(), ServiceError> { + utils::send_email("csalter@carathe.dev".to_string(), "Test Email".to_string(), "Hi, test".to_string()).await?; + + Ok(()) +} + async fn statics(Path(name): Path) -> Result { for s in templates::statics::STATICS { trace!("Name: {}\nContents:\n{:?}\n\n", s.name, s.content); diff --git a/src/models.rs b/src/models.rs index b367f4f..2295154 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,8 +1,6 @@ -use sqlx::FromRow; +use sqlx::{FromRow, types::ipnetwork::IpNetwork}; - - -#[derive(Debug, FromRow)] +#[derive(Debug, FromRow, Clone)] pub struct DbUser { pub id: String, pub email: String, @@ -10,3 +8,17 @@ pub struct DbUser { pub pw_hash: String, pub last_login: chrono::NaiveDateTime } + +#[derive(Debug, FromRow, Clone)] +pub struct Peer { + pub id: String, + pub addr: IpNetwork, + pub public_key: String, +} + +#[derive(Debug, FromRow, Clone)] +pub struct Network { + pub id: String, + pub subnet: IpNetwork, + pub description: Option +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..e723db8 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,51 @@ +use lettre::{Message, transport::smtp::{authentication::Credentials, extension::ClientId}, transport::smtp::AsyncSmtpTransport, AsyncTransport, Tokio1Executor}; + +use crate::{errors::ServiceError, config}; + +pub async fn send_email(to: String, subject: String, content: String) -> Result<(), ServiceError> { + let config = config::Config::init("/etc/nccd/config.toml".to_string()).unwrap(); + let email = if let Ok(e) = Message::builder() + .from(config.email.email_from.parse().unwrap()) + .to(to.parse().unwrap()) + .subject(subject) + .body(content) { + e + } else { + return Err(ServiceError::Email("Invalid email content".to_string())); + }; + let mailer: AsyncSmtpTransport; + let helo = ClientId::Domain(config.email.email_helo); + if let Some(u) = config.email.smtp_username { + let creds = Credentials::new(u, config.email.smtp_password.unwrap()); + if config.email.smtp_starttls { + mailer = AsyncSmtpTransport::::starttls_relay(&config.email.smtp_server) + .unwrap() + .credentials(creds) + .hello_name(helo) + .build(); + } else { + mailer = AsyncSmtpTransport::::builder_dangerous(&config.email.smtp_server) + .credentials(creds) + .hello_name(helo) + .build(); + } + } else { + if config.email.smtp_tls && config.email.smtp_starttls { + mailer = AsyncSmtpTransport::::starttls_relay(&config.email.smtp_server).unwrap().hello_name(helo) + .build(); + } else if config.email.smtp_tls { + mailer = AsyncSmtpTransport::::relay(&config.email.smtp_server).unwrap().hello_name(helo).build(); + } else { + mailer = AsyncSmtpTransport::::builder_dangerous(&config.email.smtp_server).hello_name(helo).build(); + } + } + + if let Err(e) = mailer.test_connection().await { + return Err(ServiceError::Email(e.to_string())); + } else { + if let Err(e) = mailer.send(email).await { + return Err(ServiceError::Email(e.to_string())); + } + } + Ok(()) +} diff --git a/templates/index.rs.html b/templates/index.rs.html index d0196df..e686b01 100644 --- a/templates/index.rs.html +++ b/templates/index.rs.html @@ -1,12 +1,69 @@ @use super::{header_html, footer_html}; -@use crate::models::DbUser; +@use crate::models::{DbUser, Peer, Network}; -@(user: Option) +@(user: Option, peers: Vec, nets: Vec) @:header_html()

NCCd (Network Communications Control Daemon)

@if user.is_some() { -

Welcome @user.unwrap().pref_name

+

Welcome @user.clone().unwrap().pref_name

+ + + + + + + + + + + + + + + + + + + + + +
KeyValue
ID@user.clone().unwrap().id
Email@user.clone().unwrap().email
Preferred Name@user.clone().unwrap().pref_name
Last Login@(user.clone().unwrap().last_login)Z
+ +
+ +

Configured Peers

+ + + + + + + @for p in peers { + + + + + + } +
IDAddressPublic Key
@p.id@p.addr@p.public_key
+
+

Configured subnets (New)

+ + + + + + + @for n in nets { + + + + + + } +
IDSubnet CIDRDescription
@n.id@n.subnet@if n.description.is_some() { @n.description.unwrap() + } else { None }
} else {

Please Log in

diff --git a/templates/new_net.rs.html b/templates/new_net.rs.html new file mode 100644 index 0000000..4628432 --- /dev/null +++ b/templates/new_net.rs.html @@ -0,0 +1,21 @@ +@use super::{header_html, footer_html}; + +@() + +@:header_html() + +

New Subnet

+ +
+
+ + +
+
+ + +
+ +
+ +@:footer_html() -- cgit v1.2.3