diff options
| -rw-r--r-- | src/main.rs | 226 | ||||
| -rw-r--r-- | src/ui.rs | 230 | 
2 files changed, 233 insertions, 223 deletions
diff --git a/src/main.rs b/src/main.rs index 6725c44..20fedf6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,8 @@ -use std::{sync::{Arc, Mutex}, str::FromStr}; -use serde::Deserialize; -use askama::Template; -use axum::{ -    extract::State, -    routing::get, -    Router, -    response::Html, -    Form, -    http::StatusCode, -}; +use std::sync::{Arc, Mutex};  use sqlx::{Connection, SqliteConnection}; -use tower_http::services::ServeDir;  mod config; +mod ui;  struct AppState {      conf : config::Config, @@ -39,217 +29,7 @@ async fn main() -> std::io::Result<()> {          db: Mutex::new(conn)      })); -    let app = Router::new() -        .route("/", get(dashboard)) -        .route("/incoming", get(incoming)) -        .route("/send", get(send)) -        .route("/settings", get(show_settings).post(post_settings)) -        .nest_service("/static", ServeDir::new("static")) -        /* requires tracing and tower, e.g. -         *  tower = { version = "0.4", features = ["util", "timeout"] } -         *  tower-http = { version = "0.5.0", features = ["add-extension", "trace"] } -         *  tracing = "0.1" -         *  tracing-subscriber = { version = "0.3", features = ["env-filter"] } -        .layer( -            ServiceBuilder::new() -                .layer(HandleErrorLayer::new(|error: BoxError| async move { -                    if error.is::<tower::timeout::error::Elapsed>() { -                        Ok(StatusCode::REQUEST_TIMEOUT) -                    } else { -                        Err(( -                            StatusCode::INTERNAL_SERVER_ERROR, -                            format!("Unhandled internal error: {error}"), -                        )) -                    } -                })) -                .timeout(Duration::from_secs(10)) -                .layer(TraceLayer::new_for_http()) -                .into_inner(), -        )*/ -        .with_state(shared_state); - -    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); -    axum::serve(listener, app).await.unwrap(); +    ui::serve(3000, shared_state).await;      Ok(())  } -#[derive(PartialEq)] -enum ActivePage { -    Dashboard, -    Incoming, -    Send, -    Settings, -} - -#[derive(Template)] -#[template(path = "dashboard.html")] -struct DashboardTemplate<'a> { -    title: &'a str, -    page: ActivePage, -    conf: config::Config, -} - -async fn dashboard(State(state): State<SharedState>) -> DashboardTemplate<'static> { -    DashboardTemplate { -        title: "Dashboard", -        conf: state.lock().unwrap().conf.clone(), -        page: ActivePage::Dashboard, -    } -} - -#[derive(Template)] -#[template(path = "incoming.html")] -struct IncomingTemplate<'a> { -    title: &'a str, -    page: ActivePage, -    conf: config::Config, -} - -async fn incoming(State(state): State<SharedState>) -> IncomingTemplate<'static> { -    IncomingTemplate { -        title: "Incoming", -        conf: state.lock().unwrap().conf.clone(), -        page: ActivePage::Incoming, -    } -} - -#[derive(Template)] -#[template(path = "send.html")] -struct SendTemplate<'a> { -    title: &'a str, -    page: ActivePage, -    conf: config::Config, -} - -async fn send(State(state): State<SharedState>) -> SendTemplate<'static> { -    SendTemplate { -        title: "Send", -        conf: state.lock().unwrap().conf.clone(), -        page: ActivePage::Send, -    } -} - -#[derive(Template)] -#[template(path = "settings.html")] -struct SettingsTemplate<'a> { -    title: &'a str, -    page: ActivePage, -    conf: config::Config, -} - -async fn show_settings(State(state): State<SharedState>) -> SettingsTemplate<'static> { -    SettingsTemplate { -        title: "Settings", -        page: ActivePage::Settings, -        conf: state.lock().unwrap().conf.clone(), -    } -} - -#[derive(Deserialize, Debug)] -struct FormConfig { -    callsign: String, -    ssid: String, -    icon: String, - -    // felinet -    // felinet_enabled is either "on" or absent. -    // According to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/checkbox -    // "If the value attribute was omitted, the default value for the checkbox is `on` [...]" -    felinet_enabled: Option<String>, -    address: String, - -    // beacon -    period_seconds: config::DurationSeconds, -    max_hops: u8, -    latitude: String, -    longitude: String, -    altitude: String, -    comment: String, -    antenna_height: String, -    antenna_gain: String, -    tx_power: String, - -    // tunnel -    tunnel_enabled: Option<String>, -    local_ip: String, -    netmask: String, -} - -fn empty_string_to_none<T: FromStr + Sync>(value: &str) -> Result<Option<T>, T::Err> { -    if value == "" { -        Ok(None) -    } -    else { -        Ok(Some(value.parse()?)) -    } -} - -impl TryFrom<FormConfig> for config::Config { -    type Error = anyhow::Error; - -    fn try_from(value: FormConfig) -> Result<Self, Self::Error> { -        Ok(config::Config { -            callsign: value.callsign, -            ssid: value.ssid.parse()?, -            icon: value.icon.parse()?, -            felinet: config::FelinetConfig { -                enabled: value.felinet_enabled.is_some(), -                address: value.address, -            }, -            beacon: config::BeaconConfig { -                period_seconds: value.period_seconds, -                max_hops: value.max_hops, -                latitude: empty_string_to_none(&value.latitude)?, -                longitude: empty_string_to_none(&value.longitude)?, -                altitude: empty_string_to_none(&value.altitude)?, -                comment: empty_string_to_none(&value.comment)?, -                antenna_height: empty_string_to_none(&value.antenna_height)?, -                antenna_gain: empty_string_to_none(&value.antenna_gain)?, -                tx_power: empty_string_to_none(&value.tx_power)?, -            }, -            tunnel: config::TunnelConfig { -                enabled: value.tunnel_enabled.is_some(), -                local_ip: value.local_ip, -                netmask: value.netmask, -            }, -        }) -    } -} - -async fn post_settings(State(state): State<SharedState>, Form(input): Form<FormConfig>) -> (StatusCode, Html<String>) { -    match config::Config::try_from(input) { -        Ok(c) => { -            match c.store() { -                Ok(()) => { -                    state.lock().unwrap().conf.clone_from(&c); - -                    (StatusCode::OK, Html( -                            r#"<!doctype html> -                            <html><head></head><body> -                            <p>Configuration updated</p> -                            <p>To <a href="/">dashboard</a></p> -                            </body></html>"#.to_owned())) -                } -                Err(e) => { -                    (StatusCode::INTERNAL_SERVER_ERROR, Html( -                            format!(r#"<!doctype html> -                            <html><head></head> -                            <body><p>Internal Server Error: Could not write config</p> -                            <p>{}</p> -                            </body> -                            </html>"#, e))) -                }, -            } - -        }, -        Err(e) => { -            (StatusCode::BAD_REQUEST, Html( -                    format!(r#"<!doctype html> -                            <html><head></head> -                            <body><p>Error interpreting POST data</p> -                            <p>{}</p> -                            </body> -                            </html>"#, e))) -        }, -    } -} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..aebcd4a --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,230 @@ +use std::str::FromStr; +use serde::Deserialize; +use askama::Template; +use axum::{ +    extract::State, +    routing::get, +    Router, +    response::Html, +    Form, +    http::StatusCode, +}; +use tower_http::services::ServeDir; + +use crate::config; +use crate::SharedState; + +pub async fn serve(port: u16, shared_state: SharedState) { +    let app = Router::new() +        .route("/", get(dashboard)) +        .route("/incoming", get(incoming)) +        .route("/send", get(send)) +        .route("/settings", get(show_settings).post(post_settings)) +        .nest_service("/static", ServeDir::new("static")) +        /* requires tracing and tower, e.g. +         *  tower = { version = "0.4", features = ["util", "timeout"] } +         *  tower-http = { version = "0.5.0", features = ["add-extension", "trace"] } +         *  tracing = "0.1" +         *  tracing-subscriber = { version = "0.3", features = ["env-filter"] } +         .layer( +         ServiceBuilder::new() +         .layer(HandleErrorLayer::new(|error: BoxError| async move { +         ,if error.is::<tower::timeout::error::Elapsed>() { +         Ok(StatusCode::REQUEST_TIMEOUT) +         } else { +         Err(( +         StatusCode::INTERNAL_SERVER_ERROR, +         format!("Unhandled internal error: {error}"), +         )) +         } +         })) +         .timeout(Duration::from_secs(10)) +         .layer(TraceLayer::new_for_http()) +         .into_inner(), +         )*/ +        .with_state(shared_state); + +    let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await.unwrap(); +    axum::serve(listener, app).await.unwrap() +} + +#[derive(PartialEq)] +enum ActivePage { +    Dashboard, +    Incoming, +    Send, +    Settings, +} + +#[derive(Template)] +#[template(path = "dashboard.html")] +struct DashboardTemplate<'a> { +    title: &'a str, +    page: ActivePage, +    conf: config::Config, +} + +async fn dashboard(State(state): State<SharedState>) -> DashboardTemplate<'static> { +    DashboardTemplate { +        title: "Dashboard", +        conf: state.lock().unwrap().conf.clone(), +        page: ActivePage::Dashboard, +    } +} + +#[derive(Template)] +#[template(path = "incoming.html")] +struct IncomingTemplate<'a> { +    title: &'a str, +    page: ActivePage, +    conf: config::Config, +} + +async fn incoming(State(state): State<SharedState>) -> IncomingTemplate<'static> { +    IncomingTemplate { +        title: "Incoming", +        conf: state.lock().unwrap().conf.clone(), +        page: ActivePage::Incoming, +    } +} + +#[derive(Template)] +#[template(path = "send.html")] +struct SendTemplate<'a> { +    title: &'a str, +    page: ActivePage, +    conf: config::Config, +} + +async fn send(State(state): State<SharedState>) -> SendTemplate<'static> { +    SendTemplate { +        title: "Send", +        conf: state.lock().unwrap().conf.clone(), +        page: ActivePage::Send, +    } +} + +#[derive(Template)] +#[template(path = "settings.html")] +struct SettingsTemplate<'a> { +    title: &'a str, +    page: ActivePage, +    conf: config::Config, +} + +async fn show_settings(State(state): State<SharedState>) -> SettingsTemplate<'static> { +    SettingsTemplate { +        title: "Settings", +        page: ActivePage::Settings, +        conf: state.lock().unwrap().conf.clone(), +    } +} + +#[derive(Deserialize, Debug)] +struct FormConfig { +    callsign: String, +    ssid: String, +    icon: String, + +    // felinet +    // felinet_enabled is either "on" or absent. +    // According to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/checkbox +    // "If the value attribute was omitted, the default value for the checkbox is `on` [...]" +    felinet_enabled: Option<String>, +    address: String, + +    // beacon +    period_seconds: config::DurationSeconds, +    max_hops: u8, +    latitude: String, +    longitude: String, +    altitude: String, +    comment: String, +    antenna_height: String, +    antenna_gain: String, +    tx_power: String, + +    // tunnel +    tunnel_enabled: Option<String>, +    local_ip: String, +    netmask: String, +} + +fn empty_string_to_none<T: FromStr + Sync>(value: &str) -> Result<Option<T>, T::Err> { +    if value == "" { +        Ok(None) +    } +    else { +        Ok(Some(value.parse()?)) +    } +} + +impl TryFrom<FormConfig> for config::Config { +    type Error = anyhow::Error; + +    fn try_from(value: FormConfig) -> Result<Self, Self::Error> { +        Ok(config::Config { +            callsign: value.callsign, +            ssid: value.ssid.parse()?, +            icon: value.icon.parse()?, +            felinet: config::FelinetConfig { +                enabled: value.felinet_enabled.is_some(), +                address: value.address, +            }, +            beacon: config::BeaconConfig { +                period_seconds: value.period_seconds, +                max_hops: value.max_hops, +                latitude: empty_string_to_none(&value.latitude)?, +                longitude: empty_string_to_none(&value.longitude)?, +                altitude: empty_string_to_none(&value.altitude)?, +                comment: empty_string_to_none(&value.comment)?, +                antenna_height: empty_string_to_none(&value.antenna_height)?, +                antenna_gain: empty_string_to_none(&value.antenna_gain)?, +                tx_power: empty_string_to_none(&value.tx_power)?, +            }, +            tunnel: config::TunnelConfig { +                enabled: value.tunnel_enabled.is_some(), +                local_ip: value.local_ip, +                netmask: value.netmask, +            }, +        }) +    } +} + +async fn post_settings(State(state): State<SharedState>, Form(input): Form<FormConfig>) -> (StatusCode, Html<String>) { +    match config::Config::try_from(input) { +        Ok(c) => { +            match c.store() { +                Ok(()) => { +                    state.lock().unwrap().conf.clone_from(&c); + +                    (StatusCode::OK, Html( +                            r#"<!doctype html> +                            <html><head></head><body> +                            <p>Configuration updated</p> +                            <p>To <a href="/">dashboard</a></p> +                            </body></html>"#.to_owned())) +                } +                Err(e) => { +                    (StatusCode::INTERNAL_SERVER_ERROR, Html( +                            format!(r#"<!doctype html> +                            <html><head></head> +                            <body><p>Internal Server Error: Could not write config</p> +                            <p>{}</p> +                            </body> +                            </html>"#, e))) +                }, +            } + +        }, +        Err(e) => { +            (StatusCode::BAD_REQUEST, Html( +                    format!(r#"<!doctype html> +                            <html><head></head> +                            <body><p>Error interpreting POST data</p> +                            <p>{}</p> +                            </body> +                            </html>"#, e))) +        }, +    } +}  | 
