Wide range of web improvements #70

Merged
glyph merged 18 commits from web_improvements into main 2022-01-17 09:35:30 +00:00
27 changed files with 991 additions and 1427 deletions

View File

@ -27,7 +27,6 @@ assets = [
["static/css/*", "/usr/share/peach-web/static/css/", "644"],
["static/icons/*", "/usr/share/peach-web/static/icons/", "644"],
["static/images/*", "/usr/share/peach-web/static/images/", "644"],
["static/js/*", "/usr/share/peach-web/static/js/", "644"],
["README.md", "/usr/share/doc/peach-web/README", "644"],
]
@ -37,18 +36,15 @@ maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.8"
lazy_static = "1.4.0"
log = "0.4"
nest = "1.0.0"
openssl = { version = "0.10", features = ["vendored"] }
peach-lib = { path = "../peach-lib" }
peach-network = { path = "../peach-network", features = ["serde_support"] }
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
percent-encoding = "2.1.0"
regex = "1"
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
snafu = "0.6"
tera = { version = "1.12.1", features = ["builtins"] }
xdg = "2.2.0"

View File

@ -25,7 +25,7 @@ Move into the repo and compile:
Run the tests:
`cargo test`
`ROCKET_DISABLE_AUTH=true PEACH_STANDALONE_MODE=false cargo test`
Move back to the `peach-workspace` directory:
@ -35,8 +35,6 @@ Run the binary:
`./target/release/peach-web`
_Note: Networking functionality requires peach-network microservice to be running._
### Environment
**Deployment Mode**
@ -47,6 +45,10 @@ The web application deployment mode is configured with the `ROCKET_ENV` environm
Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
**Configuration Mode**
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is configured with the `PEACH_STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode.
**Authentication**
Authentication is disabled in `development` mode and enabled by default when running the application in `production` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:

View File

@ -0,0 +1,36 @@
use peach_lib::{config_manager, dyndns_client};
use rocket::serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ConfigureDNSContext {
pub external_domain: String,
pub dyndns_subdomain: String,
pub enable_dyndns: bool,
pub is_dyndns_online: bool,
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ConfigureDNSContext {
pub fn build() -> ConfigureDNSContext {
// TODO: replace `unwrap` with resilient error handling
let peach_config = config_manager::load_peach_config().unwrap();
let dyndns_fulldomain = peach_config.dyn_domain;
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
let dyndns_subdomain =
dyndns_client::get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
ConfigureDNSContext {
external_domain: peach_config.external_domain,
dyndns_subdomain,
enable_dyndns: peach_config.dyn_enabled,
is_dyndns_online,
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}

View File

@ -0,0 +1,2 @@
pub mod dns;
pub mod network;

View File

@ -0,0 +1,398 @@
//! Data retrieval for the purpose of serving routes and hydrating
//! network-related HTML templates.
use std::collections::HashMap;
use rocket::{
form::FromForm,
serde::{Deserialize, Serialize},
UriDisplayQuery,
};
use peach_network::{
network,
network::{Scan, Status, Traffic},
};
use crate::{
utils::{
monitor,
monitor::{Alert, Data, Threshold},
},
AP_IFACE, WLAN_IFACE,
};
#[derive(Debug, Serialize)]
pub struct AccessPoint {
pub detail: Option<Scan>,
pub signal: Option<i32>,
pub state: String,
}
pub fn ap_state() -> String {
match network::state(&*AP_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
}
}
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
pub struct Ssid {
pub ssid: String,
}
#[derive(Debug, Deserialize, FromForm)]
pub struct WiFi {
pub ssid: String,
pub pass: String,
}
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
// modify traffic values & assign measurement unit
// based on received and transmitted values
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
// convert to GB
(traffic.received / 1_073_741_824, "GB".to_string())
} else if traffic.received > 0 {
// otherwise, convert it to MB
((traffic.received / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
// convert to GB
(traffic.transmitted / 1_073_741_824, "GB".to_string())
} else if traffic.transmitted > 0 {
((traffic.transmitted / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
Some(IfaceTraffic {
rx,
rx_unit,
tx,
tx_unit,
})
}
#[derive(Debug, Serialize)]
pub struct IfaceTraffic {
pub rx: u64,
pub rx_unit: String,
pub tx: u64,
pub tx_unit: String,
}
#[derive(Debug, Serialize)]
pub struct NetworkAlertContext {
pub alert: Alert,
pub back: Option<String>,
pub data_total: Option<Data>, // combined stored and current wifi traffic in bytes
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub threshold: Threshold,
pub title: Option<String>,
pub traffic: Option<IfaceTraffic>, // current wifi traffic in bytes (since boot)
}
impl NetworkAlertContext {
pub fn build() -> NetworkAlertContext {
let alert = monitor::get_alerts().unwrap();
// stored wifi data values as bytes
let stored_traffic = monitor::get_data().unwrap();
let threshold = monitor::get_thresholds().unwrap();
let (traffic, data_total) = match network::traffic(&*WLAN_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(t)) => {
let current_traffic = t.received + t.transmitted;
let traffic = convert_traffic(t);
let total = stored_traffic.total + current_traffic;
let data_total = Data { total };
(traffic, Some(data_total))
}
_ => (None, None),
};
NetworkAlertContext {
alert,
back: None,
data_total,
flash_name: None,
flash_msg: None,
threshold,
title: None,
traffic,
}
}
}
#[derive(Debug, Serialize)]
pub struct NetworkDetailContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub selected: Option<String>,
pub title: Option<String>,
pub saved_aps: Vec<String>,
pub wlan_ip: String,
pub wlan_networks: HashMap<String, AccessPoint>,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
}
impl NetworkDetailContext {
pub fn build() -> NetworkDetailContext {
let wlan_ip = match network::ip(&*WLAN_IFACE) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks saved in wpa_supplicant.conf
let saved_aps = wlan_list.clone();
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
Ok(rssi) => rssi,
Err(_) => None,
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&*WLAN_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&*WLAN_IFACE) {
Ok(status) => status,
// interface unavailable
_ => None,
};
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
let ssid = ap.ssid.clone();
let rssi = ap.signal_level.clone();
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().unwrap();
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
let ap_detail = AccessPoint {
detail: Some(ap),
state: "Available".to_string(),
signal: Some(quality_percent),
};
wlan_networks.insert(ssid, ap_detail);
}
for network in wlan_list {
// avoid repetition by checking that ssid is not already in list
if !wlan_networks.contains_key(&network) {
let ssid = network.clone();
let net_detail = AccessPoint {
detail: None,
state: "Not in range".to_string(),
signal: None,
};
wlan_networks.insert(ssid, net_detail);
}
}
NetworkDetailContext {
back: None,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
saved_aps,
wlan_ip,
wlan_networks,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}
#[derive(Debug, Serialize)]
pub struct NetworkListContext {
pub ap_state: String,
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
pub wlan_networks: HashMap<String, String>,
pub wlan_ssid: String,
}
impl NetworkListContext {
pub fn build() -> NetworkListContext {
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
wlan_networks.insert(ap.ssid, "Available".to_string());
}
for network in wlan_list {
// insert ssid (with state) only if it doesn't already exist
wlan_networks
.entry(network)
.or_insert_with(|| "Not in range".to_string());
}
let ap_state = match network::state(&*AP_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
NetworkListContext {
ap_state,
back: None,
flash_msg: None,
flash_name: None,
title: None,
wlan_networks,
wlan_ssid,
}
}
}
#[derive(Debug, Serialize)]
pub struct NetworkStatusContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<IfaceTraffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
// passing in the ssid of a chosen access point
pub selected: Option<String>,
pub title: Option<String>,
pub back: Option<String>,
}
impl NetworkStatusContext {
pub fn build() -> Self {
let ap_ip = match network::ip(&*AP_IFACE) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let ap_ssid = match network::ssid(&*AP_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not currently activated".to_string(),
};
let ap_state = match network::state(&*AP_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let ap_traffic = match network::traffic(&*AP_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
let wlan_ip = match network::ip(&*WLAN_IFACE) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
Ok(rssi) => rssi,
_ => None,
};
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&*WLAN_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&*WLAN_IFACE) {
Ok(status) => status,
_ => None,
};
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
NetworkStatusContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
back: None,
}
}
}

View File

@ -2,35 +2,57 @@
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};
use snafu::Snafu;
use serde_json::error::Error as JsonError;
use serde_yaml::Error as YamlError;
#[derive(Debug, Snafu)]
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
pub enum PeachWebError {
#[snafu(display("Error loading serde json"))]
Serde { source: serde_json::error::Error },
#[snafu(display("Error loading peach-config yaml"))]
YamlError { source: serde_yaml::Error },
#[snafu(display("{}", msg))]
FailedToRegisterDynDomain { msg: String },
#[snafu(display("{}: {}", source, msg))]
PeachLibError { source: PeachError, msg: String },
Json(JsonError),
Yaml(YamlError),
FailedToRegisterDynDomain(String),
PeachLib { source: PeachError, msg: String },
}
impl From<serde_json::error::Error> for PeachWebError {
fn from(err: serde_json::error::Error) -> PeachWebError {
PeachWebError::Serde { source: err }
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::Json(ref source) => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
}
}
}
impl From<serde_yaml::Error> for PeachWebError {
fn from(err: serde_yaml::Error) -> PeachWebError {
PeachWebError::YamlError { source: err }
impl std::fmt::Display for PeachWebError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
}
}
}
impl From<JsonError> for PeachWebError {
fn from(err: JsonError) -> PeachWebError {
PeachWebError::Json(err)
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
}
}
impl From<PeachError> for PeachWebError {
fn from(err: PeachError) -> PeachWebError {
PeachWebError::PeachLibError {
PeachWebError::PeachLib {
source: err,
msg: "".to_string(),
}

View File

@ -24,107 +24,41 @@
#![feature(proc_macro_hygiene, decl_macro)]
mod context;
pub mod error;
mod router;
pub mod routes;
#[cfg(test)]
mod tests;
pub mod utils;
use std::{env, process};
use lazy_static::lazy_static;
use log::{error, info};
use std::process;
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
use rocket_dyn_templates::Template;
use crate::routes::authentication::*;
use crate::routes::catchers::*;
use crate::routes::index::*;
use crate::routes::scuttlebutt::*;
use crate::routes::status::device::*;
use crate::routes::status::network::*;
use crate::routes::settings::admin::*;
use crate::routes::settings::dns::*;
use crate::routes::settings::menu::*;
use crate::routes::settings::network::*;
use crate::routes::settings::scuttlebutt::*;
use rocket::{Build, Rocket};
pub type BoxError = Box<dyn std::error::Error>;
/// Create rocket instance & mount all routes.
fn init_rocket() -> Rocket<Build> {
rocket::build()
// GENERAL HTML ROUTES
.mount(
"/",
routes![
help,
home,
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
],
)
// STATUS HTML ROUTES
.mount("/status", routes![device_status, network_status])
// ADMIN SETTINGS HTML ROUTES
.mount(
"/settings/admin",
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
change_password_post,
reset_password,
reset_password_post,
forgot_password_page,
send_password_reset_post,
],
)
// NETWORK SETTINGS HTML ROUTES
.mount(
"/settings/network",
routes![
add_credentials,
connect_wifi,
configure_dns,
configure_dns_post,
disconnect_wifi,
deploy_ap,
deploy_client,
forget_wifi,
network_home,
add_ssid,
add_wifi,
network_detail,
wifi_list,
wifi_password,
wifi_set_password,
wifi_usage,
wifi_usage_alerts,
wifi_usage_reset,
],
)
// SCUTTLEBUTT SETTINGS HTML ROUTES
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
// SCUTTLEBUTT SOCIAL HTML ROUTES
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
],
)
.mount("/", FileServer::from("static"))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
lazy_static! {
// determine run-mode from env var; default to standalone mode (aka peachpub)
static ref STANDALONE_MODE: bool = match env::var("PEACH_STANDALONE_MODE") {
// parse the value to a boolean; default to true for any error
Ok(val) => val.parse().unwrap_or(true),
Err(_) => true
};
}
static WLAN_IFACE: &str = "wlan0";
static AP_IFACE: &str = "ap0";
pub fn init_rocket() -> Rocket<Build> {
info!("Initializing Rocket");
if *STANDALONE_MODE {
router::build_minimal_rocket()
} else {
router::build_complete_rocket()
}
}
/// Launch the peach-web rocket server.
@ -134,7 +68,6 @@ async fn main() {
env_logger::init();
// initialize rocket
info!("Initializing Rocket");
let rocket = init_rocket();
// launch rocket

142
peach-web/src/router.rs Normal file
View File

@ -0,0 +1,142 @@
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
use rocket_dyn_templates::Template;
use crate::routes::{
authentication::*,
catchers::*,
index::*,
scuttlebutt::*,
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*},
status::{device::*, network::*},
};
/// Create minimal rocket instance and mount routes. This excludes settings
/// and status routes related to networking and the device (memory,
/// hard disk, CPU etc.).
pub fn build_minimal_rocket() -> Rocket<Build> {
rocket::build()
// GENERAL HTML ROUTES
.mount(
"/",
routes![
help,
home,
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
],
)
// ADMIN SETTINGS HTML ROUTES
.mount(
"/settings/admin",
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
change_password_post,
reset_password,
reset_password_post,
forgot_password_page,
send_password_reset_post,
],
)
// SCUTTLEBUTT SETTINGS HTML ROUTES
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
// SCUTTLEBUTT SOCIAL HTML ROUTES
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
],
)
// STATUS HTML ROUTES
// TODO: replace this with a route for `scuttlebutt_status`
.mount("/status", routes![device_status, network_status])
.mount("/", FileServer::from("static"))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
}
/// Create complete rocket instance and mount all routes.
pub fn build_complete_rocket() -> Rocket<Build> {
rocket::build()
// GENERAL HTML ROUTES
.mount(
"/",
routes![
help,
home,
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
],
)
// ADMIN SETTINGS HTML ROUTES
.mount(
"/settings/admin",
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
change_password_post,
reset_password,
reset_password_post,
forgot_password_page,
send_password_reset_post,
],
)
// NETWORK SETTINGS HTML ROUTES
.mount(
"/settings/network",
routes![
add_credentials,
connect_wifi,
configure_dns,
configure_dns_post,
disconnect_wifi,
deploy_ap,
deploy_client,
forget_wifi,
network_home,
add_ssid,
add_wifi,
network_detail,
wifi_list,
wifi_password,
wifi_set_password,
wifi_usage,
wifi_usage_alerts,
wifi_usage_reset,
],
)
// SCUTTLEBUTT SETTINGS HTML ROUTES
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
// SCUTTLEBUTT SOCIAL HTML ROUTES
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
],
)
// STATUS HTML ROUTES
.mount("/status", routes![device_status, network_status])
.mount("/", FileServer::from("static"))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
}

View File

@ -1,14 +1,17 @@
use log::info;
use rocket::form::{Form, FromForm};
use rocket::http::{Cookie, CookieJar, Status};
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Flash, Redirect};
use rocket::serde::{Deserialize, Serialize};
use rocket::{get, post, Config};
use rocket_dyn_templates::Template;
use rocket::{
form::{Form, FromForm},
get,
http::{Cookie, CookieJar, Status},
post,
request::{self, FlashMessage, FromRequest, Request},
response::{Flash, Redirect},
serde::Deserialize,
Config,
};
use rocket_dyn_templates::{tera::Context, Template};
use peach_lib::error::PeachError;
use peach_lib::password_utils;
use peach_lib::{error::PeachError, password_utils};
use crate::error::PeachWebError;
use crate::utils::TemplateOrRedirect;
@ -66,37 +69,19 @@ impl<'r> FromRequest<'r> for Authenticated {
// HELPERS AND ROUTES FOR /login
#[derive(Debug, Serialize)]
pub struct LoginContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl LoginContext {
pub fn build() -> LoginContext {
LoginContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/login")]
pub fn login(flash: Option<FlashMessage>) -> Template {
let mut context = LoginContext::build();
context.back = Some("/".to_string());
context.title = Some("Login".to_string());
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Login".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("login", &context)
Template::render("login", &context.into_json())
}
#[derive(Debug, Deserialize, FromForm)]
@ -116,8 +101,7 @@ pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
#[post("/login", data = "<login_form>")]
pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> TemplateOrRedirect {
let result = verify_login_form(login_form.into_inner());
match result {
match verify_login_form(login_form.into_inner()) {
Ok(_) => {
// if successful login, add a cookie indicating the user is authenticated
// and redirect to home page
@ -125,17 +109,18 @@ pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Templ
// is just admin (this is arbitrary).
// If we had multiple users, we could put the user_id here.
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME));
TemplateOrRedirect::Redirect(Redirect::to("/"))
}
Err(_) => {
// if unsuccessful login, render /login page again
let mut context = LoginContext::build();
context.back = Some("/".to_string());
context.title = Some("Login".to_string());
context.flash_name = Some("error".to_string());
let flash_msg = "Invalid password".to_string();
context.flash_msg = Some(flash_msg);
TemplateOrRedirect::Template(Template::render("login", &context))
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Login".to_string()));
context.insert("flash_name", &("error".to_string()));
context.insert("flash_msg", &("Invalid password".to_string()));
TemplateOrRedirect::Template(Template::render("login", &context.into_json()))
}
}
}
@ -159,44 +144,6 @@ pub struct ResetPasswordForm {
pub new_password2: String,
}
#[derive(Debug, Serialize)]
pub struct ResetPasswordContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ResetPasswordContext {
pub fn build() -> ResetPasswordContext {
ResetPasswordContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
#[derive(Debug, Serialize)]
pub struct ChangePasswordContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ChangePasswordContext {
pub fn build() -> ChangePasswordContext {
ChangePasswordContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password.
pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> {
info!(
@ -218,81 +165,63 @@ pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(),
/// and is specifically for users who have forgotten their password.
#[get("/reset_password")]
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
let mut context = ResetPasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Reset Password".to_string());
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Reset Password".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/admin/reset_password", &context)
Template::render("settings/admin/reset_password", &context.into_json())
}
/// Password reset form request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password.
#[post("/reset_password", data = "<reset_password_form>")]
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
let result = save_reset_password_form(reset_password_form.into_inner());
match result {
Ok(_) => {
let mut context = ChangePasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Reset Password".to_string());
context.flash_name = Some("success".to_string());
let flash_msg = "New password is now saved. Return home to login".to_string();
context.flash_msg = Some(flash_msg);
Template::render("settings/admin/reset_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/".to_string());
context.title = Some("Reset Password".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to reset password: {}", err));
Template::render("settings/admin/reset_password", &context)
}
}
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Reset Password".to_string()));
let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) {
Ok(_) => (
"success".to_string(),
"New password is now saved. Return home to login".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to reset password: {}", err),
),
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/admin/reset_password", &context.into_json())
}
// HELPERS AND ROUTES FOR /send_password_reset
#[derive(Debug, Serialize)]
pub struct SendPasswordResetContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl SendPasswordResetContext {
pub fn build() -> SendPasswordResetContext {
SendPasswordResetContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Page for users who have forgotten their password.
/// This route is used by a user who is not logged in
/// to initiate the sending of a new password reset.
#[get("/forgot_password")]
pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
let mut context = SendPasswordResetContext::build();
context.back = Some("/".to_string());
context.title = Some("Send Password Reset".to_string());
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Send Password Reset".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/admin/forgot_password", &context)
Template::render("settings/admin/forgot_password", &context.into_json())
}
/// Send password reset request handler. This route is used by a user who is not logged in
@ -301,27 +230,25 @@ pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
#[post("/send_password_reset")]
pub fn send_password_reset_post() -> Template {
info!("++ send password reset post");
let result = password_utils::send_password_reset();
match result {
Ok(_) => {
let mut context = ChangePasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Send Password Reset".to_string());
context.flash_name = Some("success".to_string());
let flash_msg =
"A password reset link has been sent to the admin of this device".to_string();
context.flash_msg = Some(flash_msg);
Template::render("settings/admin/forgot_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
context.back = Some("/".to_string());
context.title = Some("Send Password Reset".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to send password reset link: {}", err));
Template::render("settings/admin/forgot_password", &context)
}
}
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Send Password Reset".to_string()));
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
Ok(_) => (
"success".to_string(),
"A password reset link has been sent to the admin of this device".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to send password reset link: {}", err),
),
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/admin/forgot_password", &context.into_json())
}
// HELPERS AND ROUTES FOR /settings/change_password
@ -353,42 +280,40 @@ pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebErr
/// Change password request handler. This is used by a user who is already logged in.
#[get("/change_password")]
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
let mut context = Context::new();
context.insert("back", &Some("/settings/admin".to_string()));
context.insert("title", &Some("Change Password".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/admin/change_password", &context)
Template::render("settings/admin/change_password", &context.into_json())
}
/// Change password form request handler. This route is used by a user who is already logged in.
#[post("/change_password", data = "<password_form>")]
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
let result = save_password_form(password_form.into_inner());
match result {
Ok(_) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
context.flash_name = Some("success".to_string());
context.flash_msg = Some("New password is now saved".to_string());
// template_dir is set in Rocket.toml
Template::render("settings/admin/change_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to save new password: {}", err));
Template::render("settings/admin/change_password", &context)
}
}
let mut context = Context::new();
context.insert("back", &Some("/settings/admin".to_string()));
context.insert("title", &Some("Change Password".to_string()));
let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) {
Ok(_) => (
"success".to_string(),
"New password is now saved".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to save new password: {}", err),
),
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/admin/change_password", &context.into_json())
}

View File

@ -1,69 +1,33 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template;
use serde::Serialize;
use rocket_dyn_templates::{tera::Context, Template};
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR / (HOME PAGE)
#[derive(Debug, Serialize)]
pub struct HomeContext {
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl HomeContext {
pub fn build() -> HomeContext {
HomeContext {
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/")]
pub fn home(_auth: Authenticated) -> Template {
let context = HomeContext {
flash_name: None,
flash_msg: None,
title: None,
};
Template::render("home", &context)
let mut context = Context::new();
context.insert("flash_name", &None::<()>);
context.insert("flash_msg", &None::<()>);
context.insert("title", &None::<()>);
Template::render("home", &context.into_json())
}
// HELPERS AND ROUTES FOR /help
#[derive(Debug, Serialize)]
pub struct HelpContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl HelpContext {
pub fn build() -> HelpContext {
HelpContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/help")]
pub fn help(flash: Option<FlashMessage>) -> Template {
let mut context = HelpContext::build();
context.back = Some("/".to_string());
context.title = Some("Help".to_string());
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Help".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("help", &context)
Template::render("help", &context.into_json())
}

View File

@ -1,95 +1,69 @@
use rocket::serde::{Deserialize, Serialize};
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
serde::Deserialize,
uri,
};
use rocket_dyn_templates::Template;
use rocket_dyn_templates::{tera::Context, Template};
use peach_lib::config_manager;
use peach_lib::config_manager::load_peach_config;
use crate::error::PeachWebError;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /settings/admin
#[derive(Debug, Serialize)]
pub struct AdminMenuContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl AdminMenuContext {
pub fn build() -> AdminMenuContext {
AdminMenuContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Administrator settings menu.
#[get("/")]
pub fn admin_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = AdminMenuContext::build();
// set back icon link to settings route
context.back = Some("/settings".to_string());
context.title = Some("Administrator Settings".to_string());
let mut context = Context::new();
context.insert("back", &Some("/settings".to_string()));
context.insert("title", &Some("Administrator Settings".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/admin/menu", &context)
Template::render("settings/admin/menu", &context.into_json())
}
// HELPERS AND ROUTES FOR /settings/admin/configure
#[derive(Debug, Serialize)]
pub struct ConfigureAdminContext {
pub ssb_admin_ids: Vec<String>,
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ConfigureAdminContext {
pub fn build() -> ConfigureAdminContext {
let peach_config = load_peach_config().unwrap();
let ssb_admin_ids = peach_config.ssb_admin_ids;
ConfigureAdminContext {
ssb_admin_ids,
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// View and delete currently configured admin.
#[get("/configure")]
pub fn configure_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = ConfigureAdminContext::build();
// set back icon link to settings route
context.back = Some("/settings/admin".to_string());
context.title = Some("Configure Admin".to_string());
let mut context = Context::new();
context.insert("back", &Some("/settings/admin".to_string()));
context.insert("title", &Some("Configure Admin".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/admin/configure_admin", &context)
// load the peach configuration vector
match config_manager::load_peach_config() {
Ok(config) => {
// retrieve the vector of ssb admin ids
let ssb_admin_ids = config.ssb_admin_ids;
context.insert("ssb_admin_ids", &ssb_admin_ids);
}
// if load fails, overwrite the flash_name and flash_msg
Err(e) => {
context.insert("flash_name", &Some("error".to_string()));
context.insert(
"flash_msg",
&Some(format!("Failed to load Peach config: {}", e)),
);
}
}
Template::render("settings/admin/configure_admin", &context.into_json())
}
// HELPERS AND ROUTES FOR /settings/admin/add
@ -99,25 +73,6 @@ pub struct AddAdminForm {
pub ssb_id: String,
}
#[derive(Debug, Serialize)]
pub struct AddAdminContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl AddAdminContext {
pub fn build() -> AddAdminContext {
AddAdminContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> {
let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?;
// if the previous line didn't throw an error then it was a success
@ -126,23 +81,24 @@ pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError
#[get("/add")]
pub fn add_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = AddAdminContext::build();
context.back = Some("/settings/admin/configure".to_string());
context.title = Some("Add Admin".to_string());
let mut context = Context::new();
context.insert("back", &Some("/settings/admin/configure".to_string()));
context.insert("title", &Some("Add Admin".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/admin/add_admin", &context)
Template::render("settings/admin/add_admin", &context.into_json())
}
#[post("/add", data = "<add_admin_form>")]
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>, _auth: Authenticated) -> Flash<Redirect> {
let result = save_add_admin_form(add_admin_form.into_inner());
let url = uri!(configure_admin);
let url = uri!("/settings/admin/configure");
match result {
Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"),
Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"),

View File

@ -3,23 +3,20 @@ use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
serde::{Deserialize, Serialize},
serde::Deserialize,
};
use rocket_dyn_templates::Template;
use peach_lib::config_manager;
use peach_lib::config_manager::load_peach_config;
use peach_lib::dyndns_client;
use peach_lib::dyndns_client::{
check_is_new_dyndns_domain, get_dyndns_subdomain, get_full_dynamic_domain,
is_dns_updater_online,
use peach_lib::{
config_manager, dyndns_client,
error::PeachError,
jsonrpc_client_core::{Error, ErrorKind},
jsonrpc_core::types::error::ErrorCode,
};
use peach_lib::error::PeachError;
use peach_lib::jsonrpc_client_core::{Error, ErrorKind};
use peach_lib::jsonrpc_core::types::error::ErrorCode;
use crate::error::PeachWebError;
use crate::routes::authentication::Authenticated;
use crate::{
context::dns::ConfigureDNSContext, error::PeachWebError, routes::authentication::Authenticated,
};
#[derive(Debug, Deserialize, FromForm)]
pub struct DnsForm {
@ -32,11 +29,12 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
// first save local configurations
config_manager::set_external_domain(&dns_form.external_domain)?;
config_manager::set_dyndns_enabled_value(dns_form.enable_dyndns)?;
// if dynamic dns is enabled and this is a new domain name, then register it
if dns_form.enable_dyndns {
let full_dynamic_domain = get_full_dynamic_domain(&dns_form.dynamic_domain);
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dns_form.dynamic_domain);
// check if this is a new domain or if its already registered
let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain)?;
let is_new_domain = dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)?;
if is_new_domain {
match dyndns_client::register_domain(&full_dynamic_domain) {
Ok(_) => {
@ -48,24 +46,22 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
info!("Failed to register dyndns domain: {:?}", err);
// json response for failed update
let msg: String = match err {
PeachError::JsonRpcClientCore(source) => {
match source {
Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
ErrorCode::ServerError(-32030) => {
format!("Error registering domain: {} was previously registered", full_dynamic_domain)
}
_ => {
format!("Failed to register dyndns domain {:?}", err)
}
},
_ => {
format!("Failed to register dyndns domain: {:?}", source)
}
PeachError::JsonRpcClientCore(Error(
ErrorKind::JsonRpcError(err),
_state,
)) => {
if let ErrorCode::ServerError(-32030) = err.code {
format!(
"Error registering domain: {} was previously registered",
full_dynamic_domain
)
} else {
"Failed to register dyndns domain".to_string()
}
}
_ => "Failed to register dyndns domain".to_string(),
};
Err(PeachWebError::FailedToRegisterDynDomain { msg })
Err(PeachWebError::FailedToRegisterDynDomain(msg))
}
}
}
@ -78,74 +74,44 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
}
}
#[derive(Debug, Serialize)]
pub struct ConfigureDNSContext {
pub external_domain: String,
pub dyndns_subdomain: String,
pub enable_dyndns: bool,
pub is_dyndns_online: bool,
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ConfigureDNSContext {
pub fn build() -> ConfigureDNSContext {
let peach_config = load_peach_config().unwrap();
let dyndns_fulldomain = peach_config.dyn_domain;
let is_dyndns_online = is_dns_updater_online().unwrap();
let dyndns_subdomain =
get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
ConfigureDNSContext {
external_domain: peach_config.external_domain,
dyndns_subdomain,
enable_dyndns: peach_config.dyn_enabled,
is_dyndns_online,
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
#[get("/dns")]
pub fn configure_dns(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Configure DNS".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/network/configure_dns", &context)
}
#[post("/dns", data = "<dns>")]
pub fn configure_dns_post(dns: Form<DnsForm>, _auth: Authenticated) -> Template {
let result = save_dns_configuration(dns.into_inner());
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Configure DNS".to_string());
match result {
Ok(_) => {
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Configure DNS".to_string());
context.flash_name = Some("success".to_string());
context.flash_msg = Some("New dynamic dns configuration is now enabled".to_string());
Template::render("settings/network/configure_dns", &context)
}
Err(err) => {
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Configure DNS".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to save dns configurations: {}", err));
Template::render("settings/network/configure_dns", &context)
}
}
Template::render("settings/network/configure_dns", &context)
}

View File

@ -1,41 +1,22 @@
use rocket::{get, request::FlashMessage, serde::Serialize};
use rocket_dyn_templates::Template;
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::{tera::Context, Template};
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /settings
#[derive(Debug, Serialize)]
pub struct SettingsMenuContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl SettingsMenuContext {
pub fn build() -> SettingsMenuContext {
SettingsMenuContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// View and delete currently configured admin.
#[get("/settings")]
pub fn settings_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = SettingsMenuContext::build();
// set back icon link to network route
context.back = Some("/".to_string());
context.title = Some("Settings".to_string());
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Settings".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/menu", &context)
Template::render("settings/menu", &context.into_json())
}

View File

@ -1,23 +1,23 @@
use log::{debug, warn};
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
serde::{Deserialize, Serialize},
serde::Deserialize,
uri, UriDisplayQuery,
};
use rocket_dyn_templates::Template;
use std::collections::HashMap;
use rocket_dyn_templates::{tera::Context, Template};
use peach_lib::network_client;
use peach_lib::network_client::{AccessPoint, Networks, Scan};
use peach_lib::stats_client::Traffic;
use peach_network::network;
use crate::routes::authentication::Authenticated;
use crate::utils::monitor;
use crate::utils::monitor::{Alert, Data, Threshold};
use crate::{
context,
context::network::{NetworkAlertContext, NetworkDetailContext, NetworkListContext},
routes::authentication::Authenticated,
utils::{monitor, monitor::Threshold},
AP_IFACE, WLAN_IFACE,
};
// STRUCTS USED BY NETWORK ROUTES
@ -50,12 +50,12 @@ pub fn wifi_usage_reset(_auth: Authenticated) -> Flash<Redirect> {
pub fn connect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_detail(ssid = ssid));
match network_client::id("wlan0", ssid) {
Ok(id) => match network_client::connect(&id, "wlan0") {
match network::id(&*WLAN_IFACE, ssid) {
Ok(Some(id)) => match network::connect(&id, &*WLAN_IFACE) {
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"),
},
Err(_) => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"),
_ => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"),
}
}
@ -63,7 +63,7 @@ pub fn connect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect
pub fn disconnect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_home);
match network_client::disable("wlan0", ssid) {
match network::disable(&*WLAN_IFACE, ssid) {
Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"),
}
@ -73,7 +73,7 @@ pub fn disconnect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redir
pub fn forget_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_home);
match network_client::forget("wlan0", ssid) {
match network::forget(&*WLAN_IFACE, ssid) {
Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"),
Err(_) => Flash::error(
Redirect::to(url),
@ -84,21 +84,19 @@ pub fn forget_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect>
#[get("/wifi/modify?<ssid>")]
pub fn wifi_password(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkAddContext {
back: Some("/settings/network/wifi".to_string()),
flash_name: None,
flash_msg: None,
selected: Some(ssid.to_string()),
title: Some("Update WiFi Password".to_string()),
};
let mut context = Context::new();
context.insert("back", &Some("/settings/network/wifi".to_string()));
context.insert("title", &Some("Update WiFi Password".to_string()));
context.insert("selected", &Some(ssid.to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/network/modify_ap", &context)
Template::render("settings/network/modify_ap", &context.into_json())
}
#[post("/wifi/modify", data = "<wifi>")]
@ -106,7 +104,7 @@ pub fn wifi_set_password(wifi: Form<WiFi>, _auth: Authenticated) -> Flash<Redire
let ssid = &wifi.ssid;
let pass = &wifi.pass;
let url = uri!(network_detail(ssid = ssid));
match network_client::update("wlan0", ssid, pass) {
match network::update(&*WLAN_IFACE, ssid, pass) {
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
Err(_) => Flash::error(
Redirect::to(url),
@ -117,174 +115,23 @@ pub fn wifi_set_password(wifi: Form<WiFi>, _auth: Authenticated) -> Flash<Redire
// HELPERS AND ROUTES FOR /settings/network
#[derive(Debug, Serialize)]
pub struct NetworkContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<Traffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_scan: Option<Vec<Scan>>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: String,
pub wlan_traffic: Option<Traffic>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
// allows for passing in the ssid of a chosen access point
// this is used in the network_detail template
pub selected: Option<String>,
// page title for header in navbar
pub title: Option<String>,
// url for back-arrow link
pub back: Option<String>,
}
impl NetworkContext {
pub fn build() -> NetworkContext {
let ap_ip = match network_client::ip("ap0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
let ap_ssid = match network_client::ssid("ap0") {
Ok(ssid) => ssid,
Err(_) => "Not currently activated".to_string(),
};
let ap_state = match network_client::state("ap0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
let ap_traffic = match network_client::traffic("ap0") {
Ok(traffic) => {
let mut t = traffic;
// modify traffic values & assign measurement unit
// based on received and transmitted values
// if received > 999 MB, convert it to GB
if t.received > 1_047_527_424 {
t.received /= 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if t.received > 0 {
// otherwise, convert it to MB
t.received = (t.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.received = 0;
t.rx_unit = Some("MB".to_string());
}
if t.transmitted > 1_047_527_424 {
t.transmitted /= 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if t.transmitted > 0 {
t.transmitted = (t.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.transmitted = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
Err(_) => None,
};
let wlan_ip = match network_client::ip("wlan0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
let wlan_rssi = match network_client::rssi_percent("wlan0") {
Ok(rssi) => Some(rssi),
Err(_) => None,
};
let wlan_scan = match network_client::available_networks("wlan0") {
Ok(networks) => {
let scan: Vec<Scan> = serde_json::from_str(networks.as_str())
.expect("Failed to deserialize scan_networks response");
Some(scan)
}
Err(_) => None,
};
let wlan_ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "Not connected".to_string(),
};
let wlan_state = match network_client::state("wlan0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_status = match network_client::status("wlan0") {
Ok(status) => status,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_traffic = match network_client::traffic("wlan0") {
Ok(traffic) => {
let mut t = traffic;
// modify traffic values & assign measurement unit
// based on received and transmitted values
// if received > 999 MB, convert it to GB
if t.received > 1_047_527_424 {
t.received /= 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if t.received > 0 {
// otherwise, convert it to MB
t.received = (t.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.received = 0;
t.rx_unit = Some("MB".to_string());
}
if t.transmitted > 1_047_527_424 {
t.transmitted /= 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if t.transmitted > 0 {
t.transmitted = (t.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.transmitted = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
Err(_) => None,
};
NetworkContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_scan,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
back: None,
}
}
}
#[get("/")]
pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkContext::build();
// set back button (nav) url
context.back = Some("/settings".to_string());
// set page title
context.title = Some("Network Configuration".to_string());
// assign context
let mut context = Context::new();
context.insert("back", &Some("/settings"));
context.insert("title", &Some("Network Configuration"));
context.insert("ap_state", &context::network::ap_state());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/network/menu", &context)
Template::render("settings/network/menu", &context.into_json())
}
// HELPERS AND ROUTES FOR /settings/network/ap/activate
@ -293,7 +140,7 @@ pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Templa
pub fn deploy_ap(_auth: Authenticated) -> Flash<Redirect> {
// activate the wireless access point
debug!("Activating WiFi access point.");
match network_client::activate_ap() {
match network::start_iface_service(&*AP_IFACE) {
Ok(_) => Flash::success(
Redirect::to("/settings/network"),
"Activated WiFi access point",
@ -307,252 +154,37 @@ pub fn deploy_ap(_auth: Authenticated) -> Flash<Redirect> {
// HELPERS AND ROUTES FOR /settings/network/wifi
#[derive(Debug, Serialize)]
pub struct NetworkListContext {
pub ap_state: String,
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
pub wlan_networks: HashMap<String, String>,
pub wlan_ssid: String,
}
impl NetworkListContext {
pub fn build() -> NetworkListContext {
// list of networks saved in the wpa_supplicant.conf
let wlan_list = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize scan_list response");
networks
}
Err(_) => Vec::new(),
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network_client::available_networks("wlan0") {
Ok(networks) => {
let scan: Vec<Networks> = serde_json::from_str(networks.as_str())
.expect("Failed to deserialize scan_networks response");
scan
}
Err(_) => Vec::new(),
};
let wlan_ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "Not connected".to_string(),
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
wlan_networks.insert(ap.ssid, "Available".to_string());
}
for network in wlan_list {
// insert ssid (with state) only if it doesn't already exist
wlan_networks
.entry(network.ssid)
.or_insert_with(|| "Not in range".to_string());
}
let ap_state = match network_client::state("ap0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
NetworkListContext {
ap_state,
back: None,
flash_msg: None,
flash_name: None,
title: None,
wlan_networks,
wlan_ssid,
}
}
}
#[get("/wifi")]
pub fn wifi_list(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkListContext::build();
context.back = Some("/settings/network".to_string());
context.title = Some("WiFi Networks".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("settings/network/list_aps", &context)
}
// HELPERS AND ROUTES FOR /settings/network/wifi<ssid>
#[derive(Debug, Serialize)]
pub struct NetworkDetailContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub saved_aps: Vec<Networks>,
pub selected: Option<String>,
pub title: Option<String>,
pub wlan_ip: String,
pub wlan_networks: HashMap<String, AccessPoint>,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: String,
pub wlan_traffic: Option<Traffic>,
}
impl NetworkDetailContext {
pub fn build() -> NetworkDetailContext {
let wlan_ip = match network_client::ip("wlan0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize scan_list response");
networks
}
Err(_) => Vec::new(),
};
// list of networks saved in wpa_supplicant.conf
// HACK: we're running the same function twice (wlan_list)
// see if we can implement clone for Vec<Networks> instead
let saved_aps = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize scan_list response");
networks
}
Err(_) => Vec::new(),
};
let wlan_rssi = match network_client::rssi_percent("wlan0") {
Ok(rssi) => Some(rssi),
Err(_) => None,
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network_client::available_networks("wlan0") {
Ok(networks) => {
let scan: Vec<Scan> = serde_json::from_str(networks.as_str())
.expect("Failed to deserialize scan_networks response");
scan
}
Err(_) => Vec::new(),
};
let wlan_ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "Not connected".to_string(),
};
let wlan_state = match network_client::state("wlan0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_status = match network_client::status("wlan0") {
Ok(status) => status,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_traffic = match network_client::traffic("wlan0") {
Ok(traffic) => {
let mut t = traffic;
// modify traffic values & assign measurement unit
// based on received and transmitted values
// if received > 999 MB, convert it to GB
if t.received > 1_047_527_424 {
t.received /= 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if t.received > 0 {
// otherwise, convert it to MB
t.received = (t.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.received = 0;
t.rx_unit = Some("MB".to_string());
}
if t.transmitted > 1_047_527_424 {
t.transmitted /= 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if t.transmitted > 0 {
t.transmitted = (t.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.transmitted = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
Err(_) => None,
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
let ssid = ap.ssid.clone();
let rssi = ap.signal_level.clone();
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().unwrap();
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
let ap_detail = AccessPoint {
detail: Some(ap),
state: "Available".to_string(),
signal: Some(quality_percent),
};
wlan_networks.insert(ssid, ap_detail);
}
for network in wlan_list {
// avoid repetition by checking that ssid is not already in list
if !wlan_networks.contains_key(&network.ssid) {
let ssid = network.ssid.clone();
let net_detail = AccessPoint {
detail: None,
state: "Not in range".to_string(),
signal: None,
};
wlan_networks.insert(ssid, net_detail);
}
}
NetworkDetailContext {
back: None,
flash_name: None,
flash_msg: None,
saved_aps,
selected: None,
title: None,
wlan_ip,
wlan_networks,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}
#[get("/wifi?<ssid>")]
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkDetailContext::build();
context.back = Some("/settings/network/wifi".to_string());
context.title = Some("WiFi Network".to_string());
context.selected = Some(ssid.to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("settings/network/ap_details", &context)
}
@ -562,7 +194,7 @@ pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authentica
pub fn deploy_client(_auth: Authenticated) -> Flash<Redirect> {
// activate the wireless client
debug!("Activating WiFi client mode.");
match network_client::activate_client() {
match network::start_iface_service(&*WLAN_IFACE) {
Ok(_) => Flash::success(Redirect::to("/settings/network"), "Activated WiFi client"),
Err(_) => Flash::error(
Redirect::to("/settings/network"),
@ -575,152 +207,85 @@ pub fn deploy_client(_auth: Authenticated) -> Flash<Redirect> {
#[get("/wifi/add")]
pub fn add_wifi(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Add WiFi Network".to_string());
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/network/add_ap", &context)
}
// used in /settings/network/wifi/add?<ssid>
#[derive(Debug, Serialize)]
pub struct NetworkAddContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub selected: Option<String>,
pub title: Option<String>,
}
impl NetworkAddContext {
pub fn build() -> NetworkAddContext {
NetworkAddContext {
back: None,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
}
}
Template::render("settings/network/add_ap", &context.into_json())
}
#[get("/wifi/add?<ssid>")]
pub fn add_ssid(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network/wifi".to_string());
context.selected = Some(ssid.to_string());
context.title = Some("Add WiFi Network".to_string());
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
context.insert("selected", &Some(ssid.to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/network/add_ap", &context)
Template::render("settings/network/add_ap", &context.into_json())
}
#[post("/wifi/add", data = "<wifi>")]
pub fn add_credentials(wifi: Form<WiFi>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
// check if the credentials already exist for this access point
// note: this is nicer but it's an unstable feature:
// if check_saved_aps(&wifi.ssid).contains(true)
// use unwrap_or instead, set value to false if err is returned
let creds_exist = network_client::saved_ap(&wifi.ssid).unwrap_or(false);
if creds_exist {
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg =
Some("Network credentials already exist for this access point".to_string());
context.title = Some("Add WiFi Network".to_string());
// return early from handler with "creds already exist" message
return Template::render("settings/network/add_ap", &context);
//let creds_exist = network::saved_networks(&wifi.ssid).unwrap_or(false);
let creds_exist = match network::saved_networks() {
Ok(Some(networks)) => networks.contains(&wifi.ssid),
_ => false,
};
// if credentials not found, generate and write wifi config to wpa_supplicant
match network_client::add(&wifi.ssid, &wifi.pass) {
Ok(_) => {
debug!("Added WiFi credentials.");
// force reread of wpa_supplicant.conf file with new credentials
match network_client::reconfigure() {
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
let (flash_name, flash_msg) = if creds_exist {
(
"error".to_string(),
"Network credentials already exist for this access point".to_string(),
)
} else {
match network::add(&*WLAN_IFACE, &wifi.ssid, &wifi.pass) {
Ok(_) => {
debug!("Added WiFi credentials.");
// force reread of wpa_supplicant.conf file with new credentials
match network::reconfigure() {
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
}
("success".to_string(), "Added WiFi credentials".to_string())
}
Err(e) => {
debug!("Failed to add WiFi credentials.");
("error".to_string(), format!("{}", e))
}
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network".to_string());
context.flash_name = Some("success".to_string());
context.flash_msg = Some("Added WiFi credentials".to_string());
context.title = Some("Add WiFi Network".to_string());
Template::render("settings/network/add_ap", &context)
}
Err(_) => {
debug!("Failed to add WiFi credentials.");
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some("Failed to add WiFi credentials".to_string());
context.title = Some("Add WiFi Network".to_string());
Template::render("settings/network/add_ap", &context)
}
}
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/network/add_ap", &context.into_json())
}
// HELPERS AND ROUTES FOR WIFI USAGE
#[derive(Debug, Serialize)]
pub struct NetworkAlertContext {
pub alert: Alert,
pub back: Option<String>,
pub data_total: Data, // combined stored and current wifi traffic in bytes
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub threshold: Threshold,
pub title: Option<String>,
pub traffic: Traffic, // current wifi traffic in bytes (since boot)
}
impl NetworkAlertContext {
pub fn build() -> NetworkAlertContext {
let alert = monitor::get_alerts().unwrap();
// stored wifi data values as bytes
let stored_traffic = monitor::get_data().unwrap();
let threshold = monitor::get_thresholds().unwrap();
// current wifi traffic values as bytes
let traffic = match network_client::traffic("wlan0") {
Ok(t) => t,
Err(_) => Traffic {
received: 0,
transmitted: 0,
rx_unit: None,
tx_unit: None,
},
};
let current_traffic = traffic.received + traffic.transmitted;
let total = stored_traffic.total + current_traffic;
let data_total = Data { total };
NetworkAlertContext {
alert,
back: None,
data_total,
flash_name: None,
flash_msg: None,
threshold,
title: None,
traffic,
}
}
}
#[get("/wifi/usage")]
pub fn wifi_usage(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkAlertContext::build();

View File

@ -1,162 +1,21 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template;
use serde::Serialize;
use peach_network::{
network,
network::{Status, Traffic},
};
use crate::context::network::NetworkStatusContext;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /status/network
#[derive(Debug, Serialize)]
pub struct IfaceTraffic {
pub rx: u64,
pub rx_unit: Option<String>,
pub tx: u64,
pub tx_unit: Option<String>,
}
impl IfaceTraffic {
fn default() -> Self {
IfaceTraffic {
rx: 0,
rx_unit: None,
tx: 0,
tx_unit: None,
}
}
}
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
let mut t = IfaceTraffic::default();
// modify traffic values & assign measurement units
// based on received and transmitted values.
// if received > 999 MB, convert it to GB
if traffic.received > 1_047_527_424 {
t.rx = traffic.received / 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if traffic.received > 0 {
// otherwise, convert it to MB
t.rx = (traffic.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.rx = 0;
t.rx_unit = Some("MB".to_string());
}
if traffic.transmitted > 1_047_527_424 {
t.tx = traffic.transmitted / 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if traffic.transmitted > 0 {
t.tx = (traffic.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.tx = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
#[derive(Debug, Serialize)]
pub struct NetworkContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<IfaceTraffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
// page title for header in navbar
pub title: Option<String>,
// url for back-arrow link
pub back: Option<String>,
}
impl NetworkContext {
pub fn build() -> NetworkContext {
let ap_ip = match network::ip("ap0") {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let ap_ssid = match network::ssid("ap0") {
Ok(Some(ssid)) => ssid,
_ => "Not currently activated".to_string(),
};
let ap_state = match network::state("ap0") {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let ap_traffic = match network::traffic("ap0") {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
let wlan_ip = match network::ip("wlan0") {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let wlan_rssi = match network::rssi_percent("wlan0") {
Ok(rssi) => rssi,
_ => None,
};
let wlan_ssid = match network::ssid("wlan0") {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state("wlan0") {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status("wlan0") {
Ok(status) => status,
_ => None,
};
let wlan_traffic = match network::traffic("wlan0") {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
NetworkContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
flash_name: None,
flash_msg: None,
title: None,
back: None,
}
}
}
#[get("/network")]
pub fn network_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkContext::build();
let mut context = NetworkStatusContext::build();
context.back = Some("/status".to_string());
context.title = Some("Network Status".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("status/network", &context)
}

View File

@ -3,11 +3,8 @@ use std::io::Read;
use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client;
use rocket::serde::json::{json, Value};
use rocket::{Build, Config, Rocket};
use crate::utils::build_json_response;
use super::init_rocket;
// define authentication mode
@ -328,6 +325,13 @@ fn network_settings_menu_html() {
assert!(body.contains("Network Configuration"));
}
/*
NOTE: these tests are commented-out for the moment, due to the fact that they
invoke system commands (resulting in a `sudo` password request during
test execution). see if we can find a way to test the results without
triggering the `systemctl` call.
#[test]
fn deploy_ap() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
@ -337,6 +341,16 @@ fn deploy_ap() {
assert_eq!(response.content_type(), None);
}
#[test]
fn deploy_client() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/activate").dispatch();
// check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.content_type(), None);
}
*/
#[test]
fn dns_settings_html() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
@ -373,15 +387,6 @@ fn ap_details_html() {
//assert!(body.contains("Network not found"));
}
#[test]
fn deploy_client() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/activate").dispatch();
// check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.content_type(), None);
}
#[test]
fn add_ap_html() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
@ -509,179 +514,6 @@ fn network_status_html() {
assert!(body.contains("DOWNLOAD"));
assert!(body.contains("UPLOAD"));
}
// JSON API ROUTES
#[test]
fn activate_ap() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/activate_ap")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
}
#[test]
fn activate_client() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/activate_client")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
}
#[test]
fn return_ip() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/ip")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("wlan0"));
assert!(body.contains("ap0"));
}
#[test]
fn return_rssi() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/rssi")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Not currently connected to an access point."));
}
#[test]
fn return_ssid() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/ssid")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Not currently connected to an access point."));
}
#[test]
fn return_state() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/state")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("ap0"));
assert!(body.contains("wlan0"));
assert!(body.contains("unavailable"));
}
#[test]
fn return_status() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/status")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Not currently connected to an access point."));
}
#[test]
fn scan_networks() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/wifi")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Unable to scan for networks. Interface may be deactivated."));
}
#[test]
fn add_wifi() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi")
.header(ContentType::JSON)
.body(r#"{ "ssid": "Home", "pass": "Password" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Failed to add WiFi credentials."));
}
#[test]
fn remove_wifi() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi/forget")
.header(ContentType::JSON)
.body(r#"{ "ssid": "Home" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Failed to remove WiFi network credentials."));
}
#[test]
fn new_password() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi/modify")
.header(ContentType::JSON)
.body(r#"{ "ssid": "Home", "pass": "Password" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Failed to update WiFi password."));
}
#[test]
fn ping_pong() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/ping")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("pong!"));
}
// HELPER FUNCTION TESTS
#[test]
fn test_build_json_response() {
let status = "success".to_string();
let data = json!("WiFi credentials added.".to_string());
let j: Value = build_json_response(status, Some(data), None);
assert_eq!(j["status"], "success");
assert_eq!(j["data"], "WiFi credentials added.");
assert_eq!(j["msg"], json!(null));
}
// FILE TESTS
#[test]

View File

@ -3,7 +3,7 @@
use std::convert::TryInto;
use nest::{Error, Store, Value};
use rocket::form::{FromForm};
use rocket::form::FromForm;
use rocket::serde::{Deserialize, Serialize};
use serde_json::json;

View File

@ -12,10 +12,6 @@
<!-- display error message -->
<div class="center-text flash-message font-failure" style="padding-left: 5px;">{{ flash_msg }}.</div>
{%- endif %}
<!-- share ux information with the user if JS is disabled -->
<noscript>
<p class="center-text flash-message">This website will be unresponsive while the device shuts down or reboots.</p>
</noscript>
</div>
</div>
{%- endblock card -%}

View File

@ -24,9 +24,8 @@
</div>
</a>
<!-- middle -->
<a class="middle" href="/hello">
<div class="circle circle-large">
</div>
<a class="middle">
<div class="circle circle-large"></div>
</a>
<!-- bottom-left -->
<!-- SYSTEM STATUS LINK AND ICON -->

View File

@ -5,8 +5,6 @@
<div class="card-container">
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
{%- endblock card -%}

View File

@ -10,13 +10,8 @@
<a class="button button-secondary center" href="/settings/admin/configure" title="Cancel">Cancel</a>
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
</div>
{%- endblock card -%}

View File

@ -3,7 +3,7 @@
<!-- CHANGE PASSWORD FORM -->
<div class="card center">
<div class="form-container">
<form id="changePassword" action="/settings/change_password" method="post">
<form id="changePassword" action="/settings/admin/change_password" method="post">
<!-- input for current password -->
<input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus>
<!-- input for new password -->

View File

@ -20,11 +20,7 @@
{% endif %}
<a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin">Add Admin</a>
</div>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
{%- endblock card -%}

View File

@ -14,7 +14,5 @@
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
{%- endblock card -%}

View File

@ -3,7 +3,7 @@
<!-- NETWORK ADD CREDENTIALS FORM -->
<div class="card center">
<div class="card-container">
<form id="wifiCreds" action="/network/wifi/add" method="post">
<form id="wifiCreds" action="/settings/network/wifi/add" method="post">
<!-- input for network ssid -->
<input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value="{%- if selected -%}{{ selected }}{%- endif -%}" autofocus>
<!-- input for network password -->

View File

@ -1,10 +1,19 @@
{%- extends "nav" -%}
{%- block card -%}
{# ASSIGN VARIABLES #}
{# ---------------- #}
{%- if data_total -%}
{% set data_usage_total = data_total.total / 1024 / 1024 | round -%}
{%- else -%}
{% set data_usage_total = "x" -%}
{% endif -%}
<!-- NETWORK DATA ALERTS VIEW -->
<form id="wifiAlerts" action="/settings/network/wifi/usage" class="card center" method="post">
<div class="stack capsule" style="margin-left: 2rem; margin-right: 2rem;">
<div class="flex-grid">
<label id="dataTotal" class="label-large" title="Data download total in MB">{{ data_total.total / 1024 / 1024 | round }}</label>
<label id="dataTotal" class="label-large" title="Data download total in MB">
{{ data_usage_total }}
</label>
<label class="label-small font-near-black">MB</label>
</div>
<label class="center-text label-small font-gray">USAGE TOTAL</label>

View File

@ -1,6 +0,0 @@
<!-- share ux information with the user if JS is disabled -->
<noscript>
<div class="capsule flash-message info-border">
<p class="center-text">This website may be temporarily unresponsive while settings are being saved.</p>
</div>
</noscript>