Compare commits
	
		
			5 Commits
		
	
	
		
			tilde-inte
			...
			forget_upd
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 32c91dacbe | |||
| 2c3ee5056e | |||
| 523c781b46 | |||
| da0152a725 | |||
| 7b953dd929 | 
							
								
								
									
										33
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						| @ -1,33 +0,0 @@ | ||||
| kind: pipeline | ||||
| type: docker | ||||
| name: test-on-amd64 | ||||
|  | ||||
| platform: | ||||
|   arch: amd64 | ||||
|  | ||||
| steps: | ||||
| - name: rustfmt | ||||
|   image: rust:buster | ||||
|   commands: | ||||
|   - rustup component add rustfmt | ||||
|   - cargo fmt --check | ||||
|  | ||||
| - name: clippy | ||||
|   image: rust:buster | ||||
|   commands: | ||||
|   - rustup component add clippy | ||||
|   - cargo clippy -- -D warnings | ||||
|  | ||||
| - name: test | ||||
|   image: rust:buster | ||||
|   commands: | ||||
|   - cargo test | ||||
|  | ||||
| - name: build | ||||
|   image: rust:buster | ||||
|   commands: | ||||
|   - cargo build | ||||
|  | ||||
| trigger: | ||||
|   event: | ||||
|   - pull_request | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -2,4 +2,3 @@ | ||||
| target | ||||
| *peachdeploy.sh | ||||
| *vpsdeploy.sh | ||||
| *bindeploy.sh | ||||
|  | ||||
							
								
								
									
										3153
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -1,5 +1,7 @@ | ||||
| [workspace] | ||||
|  | ||||
| members = [ | ||||
|     "peach-buttons", | ||||
|     "peach-oled", | ||||
|     "peach-lib", | ||||
|     "peach-config", | ||||
| @ -9,7 +11,5 @@ members = [ | ||||
|     "peach-monitor", | ||||
|     "peach-stats", | ||||
|     "peach-jsonrpc-server", | ||||
|     "peach-dyndns-updater", | ||||
|     "tilde-client" | ||||
|     "peach-dyndns-updater" | ||||
| ] | ||||
|  | ||||
|  | ||||
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -4,8 +4,6 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware | ||||
|  | ||||
| [**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud) | ||||
|  | ||||
| [](https://build.coopcloud.tech/PeachCloud/peach-workspace) | ||||
|  | ||||
| ## Background | ||||
|  | ||||
| - April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256) | ||||
| @ -45,19 +43,6 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware | ||||
|  - [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system | ||||
|  - [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device | ||||
|  | ||||
| ## Continuous Integration | ||||
|  | ||||
| [Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository. | ||||
|  | ||||
| The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud). | ||||
|  | ||||
| Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example: | ||||
|  | ||||
| ``` | ||||
| git commit -m "update readme [CI SKIP]" | ||||
| git push origin main | ||||
| ``` | ||||
|  | ||||
| ## Developer Diaries | ||||
|  | ||||
| - [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519` | ||||
|  | ||||
| @ -1,34 +0,0 @@ | ||||
| --- | ||||
|  | ||||
| name: "Bug Report Template" | ||||
| about: "This template is for submitting bugs." | ||||
| title: "[BUG] " | ||||
| ref: "main" | ||||
| labels: | ||||
|  | ||||
| - bug | ||||
| - "help needed" | ||||
|  | ||||
| --- | ||||
|  | ||||
| > Please fill out the sections below. | ||||
| > Be kind and objective when writing in text. | ||||
| > Thanks for the report! :) | ||||
|  | ||||
| **Brief description of the bug:** | ||||
|  | ||||
|  | ||||
| **Steps to reproduce the bug:** | ||||
|  | ||||
|  | ||||
| **Expected behaviour:** | ||||
|  | ||||
|  | ||||
| **Technical details:** | ||||
|  | ||||
| _Is peach-web running on an x86-64 or arm64 machine?_ | ||||
|  | ||||
|  | ||||
| _What operating system distribution is it running on?_ | ||||
|  | ||||
|  | ||||
| @ -1,15 +0,0 @@ | ||||
| --- | ||||
|  | ||||
| name: "Feature Suggestion Template" | ||||
| about: "This template is for submitting feature suggestions." | ||||
| title: "[FEATURE] " | ||||
| ref: "main" | ||||
| labels: | ||||
|  | ||||
| - enhancement | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Brief description of the feature you'd like to suggest:** | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "peach-config" | ||||
| version = "0.1.27" | ||||
| version = "0.1.15" | ||||
| authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"] | ||||
| edition = "2018" | ||||
| description = "Command line tool for installing, updating and configuring PeachCloud" | ||||
| @ -37,5 +37,3 @@ log = "0.4" | ||||
| lazy_static = "1.4.0" | ||||
| peach-lib = { path = "../peach-lib" } | ||||
| rpassword = "5.0" | ||||
| golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" } | ||||
| async-std = "1.10.0" | ||||
|  | ||||
| @ -3,14 +3,17 @@ | ||||
| pub const CONF: &str = "/var/lib/peachcloud/conf"; | ||||
|  | ||||
| // List of package names which are installed via apt-get | ||||
| pub const SERVICES: [&str; 8] = [ | ||||
| pub const SERVICES: [&str; 11] = [ | ||||
|     "peach-oled", | ||||
|     "peach-network", | ||||
|     "peach-stats", | ||||
|     "peach-web", | ||||
|     "peach-probe", | ||||
|     "peach-menu", | ||||
|     "peach-buttons", | ||||
|     "peach-oled", | ||||
|     "peach-monitor", | ||||
|     "peach-probe", | ||||
|     "peach-dyndns-updater", | ||||
|     "go-sbot", | ||||
|     "peach-go-sbot", | ||||
|     "peach-config", | ||||
| ]; | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,4 @@ | ||||
| #![allow(clippy::nonstandard_macro_braces)] | ||||
| use golgi::error::GolgiError; | ||||
| use peach_lib::error::PeachError; | ||||
| pub use snafu::ResultExt; | ||||
| use snafu::Snafu; | ||||
| @ -36,14 +35,6 @@ pub enum PeachConfigError { | ||||
|     ChangePasswordError { source: PeachError }, | ||||
|     #[snafu(display("Entered passwords did not match. Please try again."))] | ||||
|     InvalidPassword, | ||||
|     #[snafu(display("Error in peach lib: {}", source))] | ||||
|     PeachLibError { source: PeachError }, | ||||
|     #[snafu(display("Error in golgi: {}", source))] | ||||
|     Golgi { source: GolgiError }, | ||||
|     #[snafu(display("{}", message))] | ||||
|     CmdInputError { message: String }, | ||||
|     #[snafu(display("{}", message))] | ||||
|     WaitForSbotError { message: String }, | ||||
| } | ||||
|  | ||||
| impl From<std::io::Error> for PeachConfigError { | ||||
| @ -60,15 +51,3 @@ impl From<serde_json::Error> for PeachConfigError { | ||||
|         PeachConfigError::SerdeError { source: err } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<PeachError> for PeachConfigError { | ||||
|     fn from(err: PeachError) -> PeachConfigError { | ||||
|         PeachConfigError::PeachLibError { source: err } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<GolgiError> for PeachConfigError { | ||||
|     fn from(err: GolgiError) -> PeachConfigError { | ||||
|         PeachConfigError::Golgi { source: err } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,32 +1,40 @@ | ||||
| use regex::Regex; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use snafu::ResultExt; | ||||
| use std::collections::HashMap; | ||||
| use std::fs; | ||||
|  | ||||
| use crate::constants::{HARDWARE_CONFIG_FILE, SERVICES}; | ||||
| use crate::constants::HARDWARE_CONFIG_FILE; | ||||
| use crate::error::{FileReadError, FileWriteError, PeachConfigError}; | ||||
| use crate::utils::get_output; | ||||
| use crate::RtcOption; | ||||
|  | ||||
| /// Helper function which returns the version of a package currently installed, | ||||
| /// as an Ok(String) if found, and as an Err if not found | ||||
| pub fn get_package_version_number(package: &str) -> Result<String, PeachConfigError> { | ||||
|     let version = get_output(&["dpkg-query", "--showformat=${Version}", "--show", package])?; | ||||
|     Ok(version) | ||||
| } | ||||
|  | ||||
| /// Returns a HashMap<String, String> of all the peach-packages which are currently installed | ||||
| /// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" } | ||||
| pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError> | ||||
| { | ||||
|     // gets a list of all packages currently installed with dpkg-query | ||||
|     let peach_packages: HashMap<String, String> = SERVICES | ||||
|         .iter() | ||||
|         .filter_map(|service| { | ||||
|             let version = get_package_version_number(service); | ||||
|             match version { | ||||
|                 Ok(v) => Some((service.to_string(), v)), | ||||
|                 Err(_) => None, | ||||
|     // gets a list of all packages currently installed with dpkg | ||||
|     let packages = get_output(&["dpkg", "-l"])?; | ||||
|  | ||||
|     // this regex matches packages which contain the word peach in them | ||||
|     // and has two match groups | ||||
|     // 1. the first match group gets the package name | ||||
|     // 2. the second match group gets the version number of the package | ||||
|     let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap(); | ||||
|  | ||||
|     // the following iterator, iterates through the captures matched via the regex | ||||
|     // and for each capture, creates a value in the hash map, | ||||
|     //  which maps the name of the package, to its version number | ||||
|     // e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" } | ||||
|     let peach_packages: HashMap<String, String> = re | ||||
|         .captures_iter(&packages) | ||||
|         .filter_map(|cap| { | ||||
|             let groups = (cap.get(1), cap.get(2)); | ||||
|             match groups { | ||||
|                 (Some(package), Some(version)) => { | ||||
|                     Some((package.as_str().to_string(), version.as_str().to_string())) | ||||
|                 } | ||||
|                 _ => None, | ||||
|             } | ||||
|         }) | ||||
|         .collect(); | ||||
|  | ||||
| @ -2,15 +2,12 @@ mod change_password; | ||||
| mod constants; | ||||
| mod error; | ||||
| mod generate_manifest; | ||||
| mod publish_address; | ||||
| mod set_permissions; | ||||
| mod setup_networking; | ||||
| mod setup_peach; | ||||
| mod setup_peach_deb; | ||||
| mod status; | ||||
| mod update; | ||||
| mod utils; | ||||
| mod wait_for_sbot; | ||||
|  | ||||
| use clap::arg_enum; | ||||
| use log::error; | ||||
| @ -47,25 +44,12 @@ enum PeachConfig { | ||||
|     Update(UpdateOpts), | ||||
|  | ||||
|     /// Changes the password for the peach-web interface | ||||
|     #[structopt(name = "change-password")] | ||||
|     #[structopt(name = "changepassword")] | ||||
|     ChangePassword(ChangePasswordOpts), | ||||
|  | ||||
|     /// Updates file permissions on PeachCloud device | ||||
|     #[structopt(name = "permissions")] | ||||
|     SetPermissions, | ||||
|  | ||||
|     /// Returns sbot id if sbot is running | ||||
|     #[structopt(name = "whoami")] | ||||
|     WhoAmI, | ||||
|  | ||||
|     /// Publish domain and port. | ||||
|     /// It takes an address argument of the form host:port | ||||
|     #[structopt(name = "publish-address")] | ||||
|     PublishAddress(PublishAddressOpts), | ||||
|  | ||||
|     /// Wait for a successful connection to sbot | ||||
|     #[structopt(name = "wait-for-sbot")] | ||||
|     WaitForSbot, | ||||
| } | ||||
|  | ||||
| #[derive(StructOpt, Debug)] | ||||
| @ -106,13 +90,6 @@ pub struct ChangePasswordOpts { | ||||
|     password: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(StructOpt, Debug)] | ||||
| pub struct PublishAddressOpts { | ||||
|     /// Specify address in the form domain:port | ||||
|     #[structopt(short, long)] | ||||
|     address: String, | ||||
| } | ||||
|  | ||||
| arg_enum! { | ||||
|     /// enum options for real-time clock choices | ||||
|     #[derive(Debug)] | ||||
| @ -125,7 +102,7 @@ arg_enum! { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn run() { | ||||
| fn main() { | ||||
|     // initialize the logger | ||||
|     env_logger::init(); | ||||
|  | ||||
| @ -178,42 +155,6 @@ async fn run() { | ||||
|                     ) | ||||
|                 } | ||||
|             }, | ||||
|             PeachConfig::WhoAmI => match status::whoami().await { | ||||
|                 Ok(sbot_id) => { | ||||
|                     println!("{:?}", sbot_id); | ||||
|                     {} | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     error!("sbot whoami encountered an error: {}", err) | ||||
|                 } | ||||
|             }, | ||||
|             PeachConfig::PublishAddress(opts) => { | ||||
|                 match publish_address::publish_address(opts.address).await { | ||||
|                     Ok(_) => {} | ||||
|                     Err(err) => { | ||||
|                         error!( | ||||
|                             "peach-config encountered an error during publish address: {}", | ||||
|                             err | ||||
|                         ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|             PeachConfig::WaitForSbot => match wait_for_sbot::wait_for_sbot().await { | ||||
|                 Ok(sbot_id) => { | ||||
|                     println!("connected with sbot and found sbot_id: {:?}", sbot_id) | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     error!("peach-config did not successfully connect to sbot: {}", err) | ||||
|                 } | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Enable an async main function and execute the `run()` function, | ||||
| // catching any errors and printing them to `stderr` before exiting the | ||||
| // process. | ||||
| #[async_std::main] | ||||
| async fn main() { | ||||
|     run().await; | ||||
| } | ||||
|  | ||||
| @ -1,37 +0,0 @@ | ||||
| use crate::error::PeachConfigError; | ||||
| use golgi::kuska_ssb::api::dto::content::PubAddress; | ||||
| use golgi::messages::SsbMessageContent; | ||||
| use peach_lib::sbot::init_sbot; | ||||
|  | ||||
| /// Utility function to publish the address (domain:port) of the pub | ||||
| /// publishing the address causes the domain and port to be used for invite generation, | ||||
| /// and also gossips this pub address to their peers | ||||
| pub async fn publish_address(address: String) -> Result<(), PeachConfigError> { | ||||
|     // split address into domain:port | ||||
|     let split: Vec<&str> = address.split(':').collect(); | ||||
|     let (domain, port): (&str, &str) = (split[0], split[1]); | ||||
|     // convert port to u16 | ||||
|     let port_as_u16: u16 = port | ||||
|         .parse() | ||||
|         .map_err(|_err| PeachConfigError::CmdInputError { | ||||
|             message: "Failure to parse domain and port. Address must be of the format host:port." | ||||
|                 .to_string(), | ||||
|         })?; | ||||
|     // publish address | ||||
|     let mut sbot = init_sbot().await?; | ||||
|     let pub_id = sbot.whoami().await?; | ||||
|     // Compose a `pub` address type message. | ||||
|     let pub_address_msg = SsbMessageContent::Pub { | ||||
|         address: Some(PubAddress { | ||||
|             // Host name (can be an IP address if onboarding over WiFi). | ||||
|             host: Some(domain.to_string()), | ||||
|             // Port. | ||||
|             port: port_as_u16, | ||||
|             // Public key. | ||||
|             key: pub_id, | ||||
|         }), | ||||
|     }; | ||||
|     // Publish the `pub` address message. | ||||
|     let _pub_msg_ref = sbot.publish(pub_address_msg).await?; | ||||
|     Ok(()) | ||||
| } | ||||
| @ -1,30 +1,21 @@ | ||||
| use lazy_static::lazy_static; | ||||
|  | ||||
| use peach_lib::config_manager; | ||||
|  | ||||
| use crate::error::PeachConfigError; | ||||
| use crate::utils::cmd; | ||||
|  | ||||
| lazy_static! { | ||||
|     pub static ref PEACH_CONFIGDIR: String = config_manager::get_config_value("PEACH_CONFIGDIR") | ||||
|         .expect("Failed to load config value for PEACH_CONFIGDIR"); | ||||
|     pub static ref PEACH_WEBDIR: String = config_manager::get_config_value("PEACH_WEBDIR") | ||||
|         .expect("Failed to load config value for PEACH_WEBDIR"); | ||||
|     pub static ref PEACH_HOMEDIR: String = config_manager::get_config_value("PEACH_HOMEDIR") | ||||
|         .expect("Failed to load config value for PEACH_HOMEDIR"); | ||||
| } | ||||
| /// All configs are stored in this folder, and should be read/writeable by peach group | ||||
| /// so they can be read and written by all PeachCloud services. | ||||
| pub const CONFIGS_DIR: &str = "/var/lib/peachcloud"; | ||||
| pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web"; | ||||
|  | ||||
| /// Utility function to set correct file permissions on the PeachCloud device. | ||||
| /// Accidentally changing file permissions is a fairly common thing to happen, | ||||
| /// so this is a useful CLI function for quickly correcting anything that may be out of order. | ||||
| pub fn set_permissions() -> Result<(), PeachConfigError> { | ||||
|     println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]"); | ||||
|     cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_CONFIGDIR])?; | ||||
|     cmd(&["chown", "-R", "peach:peach", &PEACH_CONFIGDIR])?; | ||||
|     cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_WEBDIR])?; | ||||
|     cmd(&["chown", "-R", "peach:peach", &PEACH_WEBDIR])?; | ||||
|     cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_HOMEDIR])?; | ||||
|     cmd(&["chown", "-R", "peach:peach", &PEACH_HOMEDIR])?; | ||||
|     cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?; | ||||
|     cmd(&["chown", "-R", "peach", CONFIGS_DIR])?; | ||||
|     cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?; | ||||
|     cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?; | ||||
|     cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?; | ||||
|     println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]"); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ use std::fs; | ||||
|  | ||||
| use crate::error::{FileWriteError, PeachConfigError}; | ||||
| use crate::generate_manifest::save_hardware_config; | ||||
| use crate::set_permissions::set_permissions; | ||||
| use crate::setup_networking::configure_networking; | ||||
| use crate::setup_peach_deb::setup_peach_deb; | ||||
| use crate::update::update_microservices; | ||||
| @ -69,7 +68,6 @@ pub fn setup_peach( | ||||
|         "libssl-dev", | ||||
|         "nginx", | ||||
|         "wget", | ||||
|         "dnsutils", | ||||
|         "-y", | ||||
|     ])?; | ||||
|  | ||||
| @ -240,9 +238,6 @@ pub fn setup_peach( | ||||
|     info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]"); | ||||
|     save_hardware_config(i2c, rtc)?; | ||||
|  | ||||
|     info!("[ SETTING FILE PERMISSIONS ]"); | ||||
|     set_permissions()?; | ||||
|  | ||||
|     info!("[ PEACHCLOUD SETUP COMPLETE ]"); | ||||
|     info!("[ ------------------------- ]"); | ||||
|     info!("[ please reboot your device ]"); | ||||
|  | ||||
| @ -1,9 +0,0 @@ | ||||
| use crate::error::PeachConfigError; | ||||
| use peach_lib::sbot::init_sbot; | ||||
|  | ||||
| ///  Utility function to check if sbot is running via the whoami method | ||||
| pub async fn whoami() -> Result<String, PeachConfigError> { | ||||
|     let mut sbot = init_sbot().await?; | ||||
|     let sbot_id = sbot.whoami().await?; | ||||
|     Ok(sbot_id) | ||||
| } | ||||
| @ -47,8 +47,8 @@ pub fn update_microservices() -> Result<(), PeachConfigError> { | ||||
|     cmd(&["apt-get", "update"])?; | ||||
|     // filter out peach-config from list of services | ||||
|     let services_to_update: Vec<&str> = SERVICES | ||||
|         .iter() | ||||
|         .copied() | ||||
|         .to_vec() | ||||
|         .into_iter() | ||||
|         .filter(|&x| x != "peach-config") | ||||
|         .collect(); | ||||
|  | ||||
|  | ||||
| @ -1,52 +0,0 @@ | ||||
| use std::{thread, time}; | ||||
|  | ||||
| use crate::error::PeachConfigError; | ||||
| use peach_lib::sbot::init_sbot; | ||||
|  | ||||
| static MAX_NUM_ATTEMPTS: u8 = 10; | ||||
|  | ||||
| /// Utility function to wait for a successful whoami call with sbot | ||||
| /// After each attempt to call whoami it waits 2 seconds, | ||||
| /// and if after MAX_NUM_ATTEMPTS (10) there is no successful whoami call | ||||
| /// it returns an Error. Otherwise it returns Ok(sbot_id). | ||||
| pub async fn wait_for_sbot() -> Result<String, PeachConfigError> { | ||||
|     let mut num_attempts = 0; | ||||
|     let mut whoami = None; | ||||
|  | ||||
|     while num_attempts < MAX_NUM_ATTEMPTS { | ||||
|         let mut sbot = None; | ||||
|  | ||||
|         let sbot_res = init_sbot().await; | ||||
|         match sbot_res { | ||||
|             Ok(sbot_instance) => { | ||||
|                 sbot = Some(sbot_instance); | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 eprintln!("failed to connect to sbot: {:?}", err); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if sbot.is_some() { | ||||
|             let sbot_id_res = sbot.unwrap().whoami().await; | ||||
|             match sbot_id_res { | ||||
|                 Ok(sbot_id) => { | ||||
|                     whoami = Some(sbot_id); | ||||
|                     break; | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     eprintln!("whoami failed: {:?}", err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         println!("trying to connect to sbot again {:?}", num_attempts); | ||||
|         num_attempts += 1; | ||||
|  | ||||
|         let sleep_duration = time::Duration::from_secs(2); | ||||
|         thread::sleep(sleep_duration); | ||||
|     } | ||||
|  | ||||
|     whoami.ok_or(PeachConfigError::WaitForSbotError { | ||||
|         message: "Failed to find sbot_id after 10 attempts".to_string(), | ||||
|     }) | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "peach-dyndns-updater" | ||||
| version = "0.1.8" | ||||
| version = "0.1.6" | ||||
| authors = ["Max Fowler <mfowler@commoninternet.net>"] | ||||
| edition = "2018" | ||||
| description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate." | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| use log::info; | ||||
| use peach_lib::dyndns_client::dyndns_update_ip; | ||||
| use log::{info}; | ||||
|  | ||||
|  | ||||
| fn main() { | ||||
|     // initalize the logger | ||||
|  | ||||
| @ -18,8 +18,8 @@ env_logger = "0.9" | ||||
| jsonrpc-core = "18" | ||||
| jsonrpc-http-server = "18" | ||||
| log = "0.4" | ||||
| peach-stats = { path = "../peach-stats", features = ["serde_support"] } | ||||
| serde_json = "1.0.74" | ||||
| miniserde = "0.1.15" | ||||
| peach-stats = { path = "../peach-stats", features = ["miniserde_support"] } | ||||
|  | ||||
| [dev-dependencies] | ||||
| jsonrpc-test = "18" | ||||
|  | ||||
| @ -1,16 +1,12 @@ | ||||
| use std::fmt; | ||||
|  | ||||
| use jsonrpc_core::{Error as JsonRpcError, ErrorCode}; | ||||
| use serde_json::error::Error as SerdeJsonError; | ||||
|  | ||||
| use peach_stats::StatsError; | ||||
|  | ||||
| /// Custom error type encapsulating all possible errors for a JSON-RPC server | ||||
| /// and associated methods. | ||||
| #[derive(Debug)] | ||||
| pub enum JsonRpcServerError { | ||||
|     /// Failed to serialize a string from a data structure. | ||||
|     Serde(SerdeJsonError), | ||||
|     /// An error returned from the `peach-stats` library. | ||||
|     Stats(StatsError), | ||||
|     /// An expected JSON-RPC method parameter was not provided. | ||||
| @ -28,9 +24,6 @@ impl fmt::Display for JsonRpcServerError { | ||||
|             JsonRpcServerError::MissingParameter(ref source) => { | ||||
|                 write!(f, "Missing expected parameter: {}", source) | ||||
|             } | ||||
|             JsonRpcServerError::Serde(ref source) => { | ||||
|                 write!(f, "{}", source) | ||||
|             } | ||||
|             JsonRpcServerError::Stats(ref source) => { | ||||
|                 write!(f, "{}", source) | ||||
|             } | ||||
| @ -41,11 +34,6 @@ impl fmt::Display for JsonRpcServerError { | ||||
| impl From<JsonRpcServerError> for JsonRpcError { | ||||
|     fn from(err: JsonRpcServerError) -> Self { | ||||
|         match &err { | ||||
|             JsonRpcServerError::Serde(source) => JsonRpcError { | ||||
|                 code: ErrorCode::ServerError(-32002), | ||||
|                 message: format!("{}", source), | ||||
|                 data: None, | ||||
|             }, | ||||
|             JsonRpcServerError::Stats(source) => JsonRpcError { | ||||
|                 code: ErrorCode::ServerError(-32001), | ||||
|                 message: format!("{}", source), | ||||
|  | ||||
| @ -8,6 +8,7 @@ use std::result::Result; | ||||
| use jsonrpc_core::{IoHandler, Value}; | ||||
| use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; | ||||
| use log::info; | ||||
| use miniserde::json; | ||||
| use peach_stats::stats; | ||||
|  | ||||
| mod error; | ||||
| @ -29,7 +30,7 @@ pub fn run() -> Result<(), JsonRpcServerError> { | ||||
|     io.add_sync_method("cpu_stats", move |_| { | ||||
|         info!("Fetching CPU statistics."); | ||||
|         let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?; | ||||
|         let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?; | ||||
|         let json_cpu = json::to_string(&cpu); | ||||
|  | ||||
|         Ok(Value::String(json_cpu)) | ||||
|     }); | ||||
| @ -37,7 +38,7 @@ pub fn run() -> Result<(), JsonRpcServerError> { | ||||
|     io.add_sync_method("cpu_stats_percent", move |_| { | ||||
|         info!("Fetching CPU statistics as percentages."); | ||||
|         let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?; | ||||
|         let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?; | ||||
|         let json_cpu = json::to_string(&cpu); | ||||
|  | ||||
|         Ok(Value::String(json_cpu)) | ||||
|     }); | ||||
| @ -45,7 +46,7 @@ pub fn run() -> Result<(), JsonRpcServerError> { | ||||
|     io.add_sync_method("disk_usage", move |_| { | ||||
|         info!("Fetching disk usage statistics."); | ||||
|         let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?; | ||||
|         let json_disks = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?; | ||||
|         let json_disks = json::to_string(&disks); | ||||
|  | ||||
|         Ok(Value::String(json_disks)) | ||||
|     }); | ||||
| @ -53,7 +54,7 @@ pub fn run() -> Result<(), JsonRpcServerError> { | ||||
|     io.add_sync_method("load_average", move |_| { | ||||
|         info!("Fetching system load average statistics."); | ||||
|         let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?; | ||||
|         let json_avg = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?; | ||||
|         let json_avg = json::to_string(&avg); | ||||
|  | ||||
|         Ok(Value::String(json_avg)) | ||||
|     }); | ||||
| @ -61,7 +62,7 @@ pub fn run() -> Result<(), JsonRpcServerError> { | ||||
|     io.add_sync_method("mem_stats", move |_| { | ||||
|         info!("Fetching current memory statistics."); | ||||
|         let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?; | ||||
|         let json_mem = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?; | ||||
|         let json_mem = json::to_string(&mem); | ||||
|  | ||||
|         Ok(Value::String(json_mem)) | ||||
|     }); | ||||
| @ -69,7 +70,7 @@ pub fn run() -> Result<(), JsonRpcServerError> { | ||||
|     io.add_sync_method("uptime", move |_| { | ||||
|         info!("Fetching system uptime."); | ||||
|         let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?; | ||||
|         let json_uptime = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?; | ||||
|         let json_uptime = json::to_string(&uptime); | ||||
|  | ||||
|         Ok(Value::String(json_uptime)) | ||||
|     }); | ||||
|  | ||||
| @ -1,27 +1,19 @@ | ||||
| [package] | ||||
| name = "peach-lib" | ||||
| version = "1.3.4" | ||||
| version = "1.3.1" | ||||
| authors = ["Andrew Reid <glyph@mycelial.technology>"] | ||||
| edition = "2018" | ||||
|  | ||||
| [dependencies] | ||||
| async-std = "1.10" | ||||
| chrono = "0.4" | ||||
| dirs = "4.0" | ||||
| fslock="0.1" | ||||
| kuska-ssb = { git = "https://github.com/Kuska-ssb/ssb" } | ||||
| tilde-client = { path = "../tilde-client" } | ||||
| chrono = "0.4.19" | ||||
| fslock="0.1.6" | ||||
| jsonrpc-client-core = "0.5" | ||||
| jsonrpc-client-http = "0.5" | ||||
| jsonrpc-core = "8.0" | ||||
| jsonrpc_client = "0.7" | ||||
| jsonrpc-core = "8.0.1" | ||||
| log = "0.4" | ||||
| nanorand = { version = "0.6", features = ["getrandom"] } | ||||
| nanorand = "0.6.1" | ||||
| regex = "1" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| serde_yaml = "0.8" | ||||
| toml = "0.5" | ||||
| sha3 = "0.10" | ||||
| lazy_static = "1.4" | ||||
| anyhow = "1.0.86" | ||||
| sha3 = "0.10.0" | ||||
|  | ||||
							
								
								
									
										4
									
								
								peach-lib/debug/.cargo/config
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,4 @@ | ||||
| [target.aarch64-unknown-linux-gnu] | ||||
| linker = "aarch64-linux-gnu-gcc" | ||||
| objcopy = { path ="aarch64-linux-gnu-objcopy" } | ||||
| strip = { path ="aarch64-linux-gnu-strip" } | ||||
							
								
								
									
										12
									
								
								peach-lib/debug/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | ||||
| [package] | ||||
| name = "debug" | ||||
| version = "0.1.0" | ||||
| authors = ["notplants <mfowler.email@gmail.com>"] | ||||
| edition = "2018" | ||||
|  | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| peach-lib = { path = "../" } | ||||
| env_logger = "0.6" | ||||
| chrono = "0.4.19" | ||||
							
								
								
									
										65
									
								
								peach-lib/debug/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,65 @@ | ||||
| use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update }; | ||||
| use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset}; | ||||
| use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id}; | ||||
| use peach_lib::sbot_client; | ||||
| use std::process; | ||||
| use chrono::prelude::*; | ||||
|  | ||||
|  | ||||
| fn main() { | ||||
|     // initalize the logger | ||||
|     env_logger::init(); | ||||
| // | ||||
| //    println!("Hello, world its debug!"); | ||||
| //    let result = set_new_password("password3"); | ||||
| //    println!("result: {:?}", result); | ||||
| // | ||||
| //    let result = verify_password("password1"); | ||||
| //    println!("result should be error: {:?}", result); | ||||
| // | ||||
| //    let result = verify_password("password3"); | ||||
| //    println!("result should be ok: {:?}", result); | ||||
| // | ||||
| // | ||||
| //    println!("Testing temporary passwords"); | ||||
| //    let result = set_new_temporary_password("abcd"); | ||||
| //    println!("result: {:?}", result); | ||||
| // | ||||
| //    let result = verify_temporary_password("password1"); | ||||
| //    println!("result should be error: {:?}", result); | ||||
| // | ||||
| //    let result = verify_temporary_password("abcd"); | ||||
| //    println!("result should be ok: {:?}", result); | ||||
| // | ||||
|     let result = send_password_reset(); | ||||
|     println!("send password reset result should be ok: {:?}", result); | ||||
|  | ||||
| //    sbot_client::post("hi cat"); | ||||
| //    let result = sbot_client::whoami(); | ||||
| //        let result = sbot_client::create_invite(50); | ||||
| //        let result = sbot_client::post("is this working"); | ||||
| //    println!("result: {:?}", result); | ||||
| //        let result = sbot_client::post("nice we have contact"); | ||||
| //        let result = sbot_client::update_pub_name("vermont-pub"); | ||||
| //        let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519"); | ||||
| //        println!("result: {:?}", result); | ||||
|  | ||||
| //   let result = send_password_reset(); | ||||
| //    let result = add_ssb_admin_id("xyzdab"); | ||||
| //   println!("result: {:?}", result); | ||||
| //    let result = delete_ssb_admin_id("xyzdab"); | ||||
| //    println!("result: {:?}", result); | ||||
| //    let result = delete_ssb_admin_id("ab"); | ||||
| //    println!("result: {:?}", result); | ||||
|  | ||||
| ////    let result = log_successful_nsupdate(); | ||||
| ////    let result = get_num_seconds_since_successful_dns_update(); | ||||
| //    let is_online = is_dns_updater_online(); | ||||
| //    println!("is online: {:?}", is_online); | ||||
| // | ||||
| ////    let result = get_last_successful_dns_update(); | ||||
| ////    println!("result: {:?}", result); | ||||
| ////    register_domain("newquarter299.dyn.peachcloud.org"); | ||||
| //    let result = dyndns_update_ip(); | ||||
| //    println!("result: {:?}", result); | ||||
| } | ||||
| @ -1,11 +0,0 @@ | ||||
| use peach_lib::config_manager::{get_config_value, save_config_value}; | ||||
|  | ||||
| fn main() { | ||||
|     println!("Running example of PeachCloud configuration management"); | ||||
|     let v = get_config_value("ADDR").unwrap(); | ||||
|     println!("ADDR: {}", v); | ||||
|  | ||||
|     save_config_value("ADDR", "1.1.1.1"); | ||||
|     let v = get_config_value("ADDR").unwrap(); | ||||
|     println!("ADDR: {}", v); | ||||
| } | ||||
| @ -1,280 +1,148 @@ | ||||
| //! Interfaces for writing and reading PeachCloud configurations, stored in yaml. | ||||
| //! | ||||
| //! Different PeachCloud microservices import peach-lib, so that they can share | ||||
| //! this interface. | ||||
| //! | ||||
| //! Config values are looked up from three locations in this order by key name: | ||||
| //! 1. from environmental variables | ||||
| //! 2. from a configuration file | ||||
| //! 3. from default values | ||||
| //! Different PeachCloud microservices import peach-lib, so that they can share this interface. | ||||
| //! | ||||
| //! The configuration file is located at: "/var/lib/peachcloud/config.yml" | ||||
| //! unless its path is configured by setting PEACH_CONFIG_PATH env variable. | ||||
|  | ||||
| use std::collections::{BTreeMap, HashMap}; | ||||
| use std::{env, fs}; | ||||
| use std::fs; | ||||
|  | ||||
| use fslock::LockFile; | ||||
| use lazy_static::lazy_static; | ||||
| use log::debug; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::error::PeachError; | ||||
|  | ||||
| // load path to main configuration file | ||||
| // from PEACH_CONFIG_PATH if that environment variable is set | ||||
| // or using the default value if not set | ||||
| pub const DEFAULT_YAML_PATH: &str = "/var/lib/peachcloud/config.yml"; | ||||
| lazy_static! { | ||||
|     static ref CONFIG_PATH: String = { | ||||
|         if let Ok(val) = env::var("PEACH_CONFIG_PATH") { | ||||
|              val | ||||
|         } | ||||
|         else { | ||||
|             DEFAULT_YAML_PATH.to_string() | ||||
|         } | ||||
|     }; | ||||
| // main configuration file | ||||
| pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml"; | ||||
|  | ||||
| // lock file (used to avoid race conditions during config reading & writing) | ||||
|     // the lock file path is the config file path + ".lock" | ||||
|     static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH); | ||||
| pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock"; | ||||
|  | ||||
| // we make use of Serde default values in order to make PeachCloud | ||||
| // robust and keep running even with a not fully complete config.yml | ||||
| // main type which represents all peachcloud configurations | ||||
| #[derive(Debug, PartialEq, Serialize, Deserialize)] | ||||
| pub struct PeachConfig { | ||||
|     #[serde(default)] | ||||
|     pub external_domain: String, | ||||
|     #[serde(default)] | ||||
|     pub dyn_domain: String, | ||||
|     #[serde(default)] | ||||
|     pub dyn_dns_server_address: String, | ||||
|     #[serde(default)] | ||||
|     pub dyn_tsig_key_path: String, | ||||
|     #[serde(default)] // default is false | ||||
|     pub dyn_enabled: bool, | ||||
|     #[serde(default)] // default is empty vector | ||||
|     pub ssb_admin_ids: Vec<String>, | ||||
|     #[serde(default)] | ||||
|     pub admin_password_hash: String, | ||||
|     #[serde(default)] | ||||
|     pub temporary_password_hash: String, | ||||
| } | ||||
|  | ||||
| // Default values for PeachCloud configs which are used for any key which is not set | ||||
| // via an environment variable or in a saved configuration file. | ||||
| pub fn get_peach_config_defaults() -> HashMap<String, String> { | ||||
|     let peach_config_defaults: HashMap<&str, &str> = HashMap::from([ | ||||
|         ("STANDALONE_MODE", "true"), | ||||
|         ("DISABLE_AUTH", "false"), | ||||
|         ("ADDR", "127.0.0.1"), | ||||
|         ("PORT", "8000"), | ||||
|         ("EXTERNAL_DOMAIN", ""), | ||||
|         ("DYN_DOMAIN", ""), | ||||
|         ( | ||||
|             "DYN_DNS_SERVER_ADDRESS", | ||||
|             "http://dynserver.dyn.peachcloud.org", | ||||
|         ), | ||||
|         ("DYN_USE_CUSTOM_SERVER", "true"), | ||||
|         ("DYN_TSIG_KEY_PATH", ""), | ||||
|         ("DYN_NAMESERVER", "ns.peachcloud.org"), | ||||
|         ("DYN_ENABLED", "false"), | ||||
|         ("SSB_ADMIN_IDS", ""), | ||||
|         ("ADMIN_PASSWORD_HASH", "47"), | ||||
|         ("TEMPORARY_PASSWORD_HASH", ""), | ||||
|         ("TILDE_SBOT_DATADIR", "/home/notplants/.local/share/tildefriends/"), | ||||
|         ("TILDE_SBOT_SERVICE", "tilde-sbot.service"), | ||||
|         ("PEACH_CONFIGDIR", "/var/lib/peachcloud"), | ||||
|         ("PEACH_HOMEDIR", "/home/peach"), | ||||
|         ("PEACH_WEBDIR", "/usr/share/peach-web"), | ||||
|     ]); | ||||
|     // convert HashMap<&str, &str> to HashMap<String, String> and return | ||||
|     let pc_defaults: HashMap<String, String> = peach_config_defaults | ||||
|         .iter() | ||||
|         .map(|(key, val)| (key.to_string(), val.to_string())) | ||||
|         .collect(); | ||||
|     pc_defaults | ||||
| } | ||||
|  | ||||
| // primary interface for getting config values | ||||
| // Config values are looked up from three locations in this order by key name: | ||||
| // 1. from environmental variables | ||||
| // 2. from a configuration file | ||||
| // 3. from default values | ||||
| pub fn get_config_value(key: &str) -> Result<String, PeachError> { | ||||
|     // first check if there is an environmental variable set | ||||
|     if let Ok(val) = env::var(key) { | ||||
|         Ok(val) | ||||
|     } else { | ||||
|         // then check if a value is set in the config file | ||||
|         let peach_config_on_disc = load_peach_config_from_disc()?; | ||||
|         let val = peach_config_on_disc.get(key); | ||||
|         // if no value is found in the config file, then get the default value | ||||
|         match val { | ||||
|             // return config value | ||||
|             Some(v) => Ok(v.to_string()), | ||||
|             // get default value | ||||
|             None => { | ||||
|                 match get_peach_config_defaults().get(key) { | ||||
|                     Some(v) => Ok(v.to_string()), | ||||
|                     // if this key was not found in the defaults, then it was an invalid key | ||||
|                     None => Err(PeachError::InvalidKey { | ||||
|                         key: key.to_string(), | ||||
|                     }), | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // helper function to load PeachCloud configuration file saved to disc | ||||
| pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> { | ||||
|     let peach_config_exists = std::path::Path::new(CONFIG_PATH.as_str()).exists(); | ||||
|     // if config file does not exist, return an emtpy HashMap | ||||
|     if !peach_config_exists { | ||||
|         let peach_config: HashMap<String, String> = HashMap::new(); | ||||
|         Ok(peach_config) | ||||
|     } | ||||
|     // otherwise we load peach config from disk | ||||
|     else { | ||||
|         debug!("Loading peach config: {} exists", CONFIG_PATH.as_str()); | ||||
|         let contents = | ||||
|             fs::read_to_string(CONFIG_PATH.as_str()).map_err(|source| PeachError::Read { | ||||
|                 source, | ||||
|                 path: CONFIG_PATH.to_string(), | ||||
|             })?; | ||||
|         let peach_config: HashMap<String, String> = serde_yaml::from_str(&contents)?; | ||||
|         Ok(peach_config) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // helper function to save PeachCloud configuration file to disc | ||||
| // takes in a Hashmap<String, String> and saves the whole HashMap as a yaml file | ||||
| // with the keys in alphabetical order | ||||
| pub fn save_peach_config_to_disc( | ||||
|     peach_config: HashMap<String, String>, | ||||
| ) -> Result<HashMap<String, String>, PeachError> { | ||||
| // helper functions for serializing and deserializing PeachConfig from disc | ||||
| fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> { | ||||
|     // use a file lock to avoid race conditions while saving config | ||||
|     let mut lock = LockFile::open(&*LOCK_FILE_PATH).map_err(|source| PeachError::Read { | ||||
|         source, | ||||
|         path: LOCK_FILE_PATH.to_string(), | ||||
|     })?; | ||||
|     let mut lock = LockFile::open(LOCK_FILE_PATH)?; | ||||
|     lock.lock()?; | ||||
|  | ||||
|     // first convert Hashmap to BTreeMap (so that keys are saved in deterministic alphabetical order) | ||||
|     let ordered: BTreeMap<_, _> = peach_config.iter().collect(); | ||||
|     // then serialize BTreeMap as yaml | ||||
|     let yaml_str = serde_yaml::to_string(&ordered)?; | ||||
|     let yaml_str = serde_yaml::to_string(&peach_config)?; | ||||
|  | ||||
|     // write yaml to file | ||||
|     fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write { | ||||
|     fs::write(YAML_PATH, yaml_str).map_err(|source| PeachError::Write { | ||||
|         source, | ||||
|         path: CONFIG_PATH.to_string(), | ||||
|         path: YAML_PATH.to_string(), | ||||
|     })?; | ||||
|  | ||||
|     // unlock file lock | ||||
|     lock.unlock()?; | ||||
|  | ||||
|     // return modified HashMap | ||||
|     // return peach_config | ||||
|     Ok(peach_config) | ||||
| } | ||||
|  | ||||
| // helper functions for serializing and deserializing PeachConfig values from disc | ||||
| pub fn save_config_value(key: &str, value: &str) -> Result<HashMap<String, String>, PeachError> { | ||||
|     // get current config from disc | ||||
|     let mut peach_config = load_peach_config_from_disc()?; | ||||
| pub fn load_peach_config() -> Result<PeachConfig, PeachError> { | ||||
|     let peach_config_exists = std::path::Path::new(YAML_PATH).exists(); | ||||
|  | ||||
|     // insert new key/value | ||||
|     peach_config.insert(key.to_string(), value.to_string()); | ||||
|     let peach_config: PeachConfig; | ||||
|  | ||||
|     // save the modified hashmap to disc | ||||
|     save_peach_config_to_disc(peach_config) | ||||
|     // if this is the first time loading peach_config, we can create a default here | ||||
|     if !peach_config_exists { | ||||
|         peach_config = PeachConfig { | ||||
|             external_domain: "".to_string(), | ||||
|             dyn_domain: "".to_string(), | ||||
|             dyn_dns_server_address: "".to_string(), | ||||
|             dyn_tsig_key_path: "".to_string(), | ||||
|             dyn_enabled: false, | ||||
|             ssb_admin_ids: Vec::new(), | ||||
|             admin_password_hash: "".to_string(), | ||||
|             temporary_password_hash: "".to_string(), | ||||
|         }; | ||||
|     } | ||||
|     // otherwise we load peach config from disk | ||||
|     else { | ||||
|         let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read { | ||||
|             source, | ||||
|             path: YAML_PATH.to_string(), | ||||
|         })?; | ||||
|         peach_config = serde_yaml::from_str(&contents)?; | ||||
|     } | ||||
|  | ||||
| // set all dyn configuration values at once | ||||
|     Ok(peach_config) | ||||
| } | ||||
|  | ||||
| // interfaces for setting specific config values | ||||
| pub fn set_peach_dyndns_config( | ||||
|     dyn_domain: &str, | ||||
|     dyn_dns_server_address: &str, | ||||
|     dyn_tsig_key_path: &str, | ||||
|     dyn_enabled: bool, | ||||
| ) -> Result<HashMap<String, String>, PeachError> { | ||||
|     let mut peach_config = load_peach_config_from_disc()?; | ||||
|     let dyn_enabled_str = match dyn_enabled { | ||||
|         true => "true", | ||||
|         false => "false", | ||||
|     }; | ||||
|     peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string()); | ||||
|     peach_config.insert( | ||||
|         "DYN_DNS_SERVER_ADDRESS".to_string(), | ||||
|         dyn_dns_server_address.to_string(), | ||||
|     ); | ||||
|     peach_config.insert( | ||||
|         "DYN_TSIG_KEY_PATH".to_string(), | ||||
|         dyn_tsig_key_path.to_string(), | ||||
|     ); | ||||
|     peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string()); | ||||
|     save_peach_config_to_disc(peach_config) | ||||
| ) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     peach_config.dyn_domain = dyn_domain.to_string(); | ||||
|     peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string(); | ||||
|     peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string(); | ||||
|     peach_config.dyn_enabled = dyn_enabled; | ||||
|     save_peach_config(peach_config) | ||||
| } | ||||
|  | ||||
| pub fn set_external_domain( | ||||
|     new_external_domain: &str, | ||||
| ) -> Result<HashMap<String, String>, PeachError> { | ||||
|     save_config_value("EXTERNAL_DOMAIN", new_external_domain) | ||||
| pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     peach_config.external_domain = new_external_domain.to_string(); | ||||
|     save_peach_config(peach_config) | ||||
| } | ||||
|  | ||||
| pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> { | ||||
|     let external_domain = get_config_value("EXTERNAL_DOMAIN")?; | ||||
|     let dyn_domain = get_config_value("DYN_DOMAIN")?; | ||||
|     if !external_domain.is_empty() { | ||||
|         Ok(Some(external_domain)) | ||||
|     } else if !dyn_domain.is_empty() { | ||||
|         Ok(Some(dyn_domain)) | ||||
|     let peach_config = load_peach_config()?; | ||||
|     if !peach_config.external_domain.is_empty() { | ||||
|         Ok(Some(peach_config.external_domain)) | ||||
|     } else if !peach_config.dyn_domain.is_empty() { | ||||
|         Ok(Some(peach_config.dyn_domain)) | ||||
|     } else { | ||||
|         Ok(None) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn get_dyndns_server_address() -> Result<String, PeachError> { | ||||
|     get_config_value("DYN_DNS_SERVER_ADDRESS") | ||||
| pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     peach_config.dyn_enabled = enabled_value; | ||||
|     save_peach_config(peach_config) | ||||
| } | ||||
|  | ||||
| pub fn set_dyndns_enabled_value( | ||||
|     enabled_value: bool, | ||||
| ) -> Result<HashMap<String, String>, PeachError> { | ||||
|     match enabled_value { | ||||
|         true => save_config_value("DYN_ENABLED", "true"), | ||||
|         false => save_config_value("DYN_ENABLED", "false"), | ||||
|     } | ||||
| pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     peach_config.ssb_admin_ids.push(ssb_id.to_string()); | ||||
|     save_peach_config(peach_config) | ||||
| } | ||||
|  | ||||
| pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> { | ||||
|     let val = get_config_value("DYN_ENABLED")?; | ||||
|     Ok(val == "true") | ||||
| } | ||||
|  | ||||
| pub fn set_admin_password_hash( | ||||
|     password_hash: String, | ||||
| ) -> Result<HashMap<String, String>, PeachError> { | ||||
|     save_config_value("ADMIN_PASSWORD_HASH", &password_hash) | ||||
| } | ||||
|  | ||||
| pub fn get_admin_password_hash() -> Result<String, PeachError> { | ||||
|     let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?; | ||||
|     if !admin_password_hash.is_empty() { | ||||
|         Ok(admin_password_hash) | ||||
|     } else { | ||||
|         Err(PeachError::PasswordNotSet) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn set_temporary_password_hash( | ||||
|     password_hash: String, | ||||
| ) -> Result<HashMap<String, String>, PeachError> { | ||||
|     save_config_value("TEMPORARY_PASSWORD_HASH", &password_hash) | ||||
| } | ||||
|  | ||||
| pub fn get_temporary_password_hash() -> Result<String, PeachError> { | ||||
|     let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?; | ||||
|     if !admin_password_hash.is_empty() { | ||||
|         Ok(admin_password_hash) | ||||
|     } else { | ||||
|         Err(PeachError::PasswordNotSet) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // add ssb_id to vector of admin ids and save new value for SSB_ADMIN_IDS | ||||
| pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> { | ||||
|     let mut ssb_admin_ids = get_ssb_admin_ids()?; | ||||
|     ssb_admin_ids.push(ssb_id.to_string()); | ||||
|     save_ssb_admin_ids(ssb_admin_ids) | ||||
| } | ||||
|  | ||||
| // remove ssb_id from vector of admin ids if found and save new value for SSB_ADMIN_IDS | ||||
| // if value is not found then return an error | ||||
| pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> { | ||||
|     let mut ssb_admin_ids = get_ssb_admin_ids()?; | ||||
| pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     let mut ssb_admin_ids = peach_config.ssb_admin_ids; | ||||
|     let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id); | ||||
|     match index_result { | ||||
|         Some(index) => { | ||||
|             ssb_admin_ids.remove(index); | ||||
|             save_ssb_admin_ids(ssb_admin_ids) | ||||
|             peach_config.ssb_admin_ids = ssb_admin_ids; | ||||
|             save_peach_config(peach_config) | ||||
|         } | ||||
|         None => Err(PeachError::SsbAdminIdNotFound { | ||||
|             id: ssb_id.to_string(), | ||||
| @ -282,16 +150,32 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| // looks up the String value for SSB_ADMIN_IDS and converts it into a Vec<String> | ||||
| pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> { | ||||
|     let ssb_admin_ids_str = get_config_value("SSB_ADMIN_IDS")?; | ||||
|     let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?; | ||||
|     Ok(ssb_admin_ids) | ||||
| pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     peach_config.admin_password_hash = password_hash.to_string(); | ||||
|     save_peach_config(peach_config) | ||||
| } | ||||
|  | ||||
| // takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec | ||||
| pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> { | ||||
|     let ssb_admin_ids_as_json_str = serde_json::to_string(&ssb_admin_ids)?; | ||||
|     save_config_value("SSB_ADMIN_IDS", &ssb_admin_ids_as_json_str)?; | ||||
|     Ok(ssb_admin_ids) | ||||
| pub fn get_admin_password_hash() -> Result<String, PeachError> { | ||||
|     let peach_config = load_peach_config()?; | ||||
|     if !peach_config.admin_password_hash.is_empty() { | ||||
|         Ok(peach_config.admin_password_hash) | ||||
|     } else { | ||||
|         Err(PeachError::PasswordNotSet) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> { | ||||
|     let mut peach_config = load_peach_config()?; | ||||
|     peach_config.temporary_password_hash = password_hash.to_string(); | ||||
|     save_peach_config(peach_config) | ||||
| } | ||||
|  | ||||
| pub fn get_temporary_password_hash() -> Result<String, PeachError> { | ||||
|     let peach_config = load_peach_config()?; | ||||
|     if !peach_config.temporary_password_hash.is_empty() { | ||||
|         Ok(peach_config.temporary_password_hash) | ||||
|     } else { | ||||
|         Err(PeachError::PasswordNotSet) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -9,8 +9,13 @@ | ||||
| //! | ||||
| //! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml | ||||
| //! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key | ||||
| use std::ffi::OsStr; | ||||
| use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr}; | ||||
| use std::{ | ||||
|     fs, | ||||
|     fs::OpenOptions, | ||||
|     io::Write, | ||||
|     process::{Command, Stdio}, | ||||
|     str::FromStr, | ||||
| }; | ||||
|  | ||||
| use chrono::prelude::*; | ||||
| use jsonrpc_client_core::{expand_params, jsonrpc_client}; | ||||
| @ -18,12 +23,13 @@ use jsonrpc_client_http::HttpTransport; | ||||
| use log::{debug, info}; | ||||
| use regex::Regex; | ||||
|  | ||||
| use crate::config_manager::{ | ||||
|     get_config_value, get_dyndns_enabled_value, get_dyndns_server_address, | ||||
| use crate::{ | ||||
|     config_manager::{load_peach_config, set_peach_dyndns_config}, | ||||
|     error::PeachError, | ||||
| }; | ||||
| use crate::{config_manager, error::PeachError}; | ||||
|  | ||||
| /// constants for dyndns configuration | ||||
| pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org"; | ||||
| pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key"; | ||||
| pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns"; | ||||
| pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log"; | ||||
| @ -56,10 +62,9 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> { | ||||
| pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> { | ||||
|     debug!("Creating HTTP transport for dyndns client."); | ||||
|     let transport = HttpTransport::new().standalone()?; | ||||
|     let http_server = get_dyndns_server_address()?; | ||||
|     info!("Using dyndns http server address: {:?}", http_server); | ||||
|     debug!("Creating HTTP transport handle on {}.", &http_server); | ||||
|     let transport_handle = transport.handle(&http_server)?; | ||||
|     let http_server = PEACH_DYNDNS_URL; | ||||
|     debug!("Creating HTTP transport handle on {}.", http_server); | ||||
|     let transport_handle = transport.handle(http_server)?; | ||||
|     info!("Creating client for peach-dyndns service."); | ||||
|     let mut client = PeachDynDnsClient::new(transport_handle); | ||||
|  | ||||
| @ -68,8 +73,7 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> | ||||
|     // save new TSIG key | ||||
|     save_dyndns_key(&key)?; | ||||
|     // save new configuration values | ||||
|     let set_config_result = | ||||
|         config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true); | ||||
|     let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true); | ||||
|     match set_config_result { | ||||
|         Ok(_) => { | ||||
|             let response = "success".to_string(); | ||||
| @ -83,9 +87,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> | ||||
| pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> { | ||||
|     debug!("Creating HTTP transport for dyndns client."); | ||||
|     let transport = HttpTransport::new().standalone()?; | ||||
|     let http_server = get_dyndns_server_address()?; | ||||
|     debug!("Creating HTTP transport handle on {}.", &http_server); | ||||
|     let transport_handle = transport.handle(&http_server)?; | ||||
|     let http_server = PEACH_DYNDNS_URL; | ||||
|     debug!("Creating HTTP transport handle on {}.", http_server); | ||||
|     let transport_handle = transport.handle(http_server)?; | ||||
|     info!("Creating client for peach_network service."); | ||||
|     let mut client = PeachDynDnsClient::new(transport_handle); | ||||
|  | ||||
| @ -101,7 +105,7 @@ pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError | ||||
| /// Helper function to get public ip address of PeachCloud device. | ||||
| fn get_public_ip_address() -> Result<String, PeachError> { | ||||
|     // TODO: consider other ways to get public IP address | ||||
|     let output = Command::new("curl").arg("ifconfig.me").output()?; | ||||
|     let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?; | ||||
|     let command_output = String::from_utf8(output.stdout)?; | ||||
|     Ok(command_output) | ||||
| } | ||||
| @ -109,28 +113,31 @@ fn get_public_ip_address() -> Result<String, PeachError> { | ||||
| /// Reads dyndns configurations from config.yml | ||||
| /// and then uses nsupdate to update the IP address for the configured domain | ||||
| pub fn dyndns_update_ip() -> Result<bool, PeachError> { | ||||
|     let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?; | ||||
|     let dyn_enabled = get_dyndns_enabled_value()?; | ||||
|     let dyn_domain = get_config_value("DYN_DOMAIN")?; | ||||
|     let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?; | ||||
|     let dyn_nameserver = get_config_value("DYN_NAMESERVER")?; | ||||
|     info!("Running dyndns_update_ip"); | ||||
|     let peach_config = load_peach_config()?; | ||||
|     info!( | ||||
|         "Using config: | ||||
|     dyn_tsig_key_path: {:?} | ||||
|     dyn_domain: {:?} | ||||
|     dyn_dns_server_address: {:?} | ||||
|     dyn_enabled: {:?} | ||||
|     dyn_nameserver: {:?} | ||||
|     ", | ||||
|         dyn_tsig_key_path, dyn_domain, dyn_dns_server_address, dyn_enabled, dyn_nameserver, | ||||
|         peach_config.dyn_tsig_key_path, | ||||
|         peach_config.dyn_domain, | ||||
|         peach_config.dyn_dns_server_address, | ||||
|         peach_config.dyn_enabled, | ||||
|     ); | ||||
|     if !dyn_enabled { | ||||
|     if !peach_config.dyn_enabled { | ||||
|         info!("dyndns is not enabled, not updating"); | ||||
|         Ok(false) | ||||
|     } else { | ||||
|         // call nsupdate passing appropriate configs | ||||
|         let mut nsupdate_command = Command::new("nsupdate"); | ||||
|         nsupdate_command.arg("-k").arg(&dyn_tsig_key_path).arg("-v"); | ||||
|         let mut nsupdate_command = Command::new("/usr/bin/nsupdate") | ||||
|             .arg("-k") | ||||
|             .arg(&peach_config.dyn_tsig_key_path) | ||||
|             .arg("-v") | ||||
|             .stdin(Stdio::piped()) | ||||
|             .spawn()?; | ||||
|         // pass nsupdate commands via stdin | ||||
|         let public_ip_address = get_public_ip_address()?; | ||||
|         info!("found public ip address: {}", public_ip_address); | ||||
| @ -141,20 +148,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> { | ||||
|         update delete {DOMAIN} A | ||||
|         update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS} | ||||
|         send", | ||||
|             NAMESERVER = dyn_nameserver, | ||||
|             ZONE = dyn_domain, | ||||
|             DOMAIN = dyn_domain, | ||||
|             NAMESERVER = "ns.peachcloud.org", | ||||
|             ZONE = peach_config.dyn_domain, | ||||
|             DOMAIN = peach_config.dyn_domain, | ||||
|             PUBLIC_IP_ADDRESS = public_ip_address, | ||||
|         ); | ||||
|         info!("ns_commands: {:?}", ns_commands); | ||||
|         info!("creating nsupdate temp file"); | ||||
|         let temp_file_path = "/var/lib/peachcloud/nsupdate.sh"; | ||||
|         // write ns_commands to temp_file | ||||
|         fs::write(temp_file_path, ns_commands)?; | ||||
|         nsupdate_command.arg(temp_file_path); | ||||
|         let nsupdate_output = nsupdate_command.output()?; | ||||
|         let args: Vec<&OsStr> = nsupdate_command.get_args().collect(); | ||||
|         info!("nsupdate command: {:?}", args); | ||||
|         let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate { | ||||
|             msg: "unable to capture stdin handle for `nsupdate` command".to_string(), | ||||
|         })?; | ||||
|         write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write { | ||||
|             source, | ||||
|             path: peach_config.dyn_tsig_key_path.to_string(), | ||||
|         })?; | ||||
|         let nsupdate_output = nsupdate_command.wait_with_output()?; | ||||
|         info!("nsupdate output: {:?}", nsupdate_output); | ||||
|         // We only return a successful result if nsupdate was successful | ||||
|         if nsupdate_output.status.success() { | ||||
|             info!("nsupdate succeeded, returning ok"); | ||||
| @ -197,7 +204,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac | ||||
|         })?; | ||||
|         // replace newline if found | ||||
|         // TODO: maybe we can use `.trim()` instead | ||||
|         let contents = contents.replace('\n', ""); | ||||
|         let contents = contents.replace("\n", ""); | ||||
|         // TODO: consider adding additional context? | ||||
|         let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| { | ||||
|             PeachError::ParseDateTime { | ||||
| @ -216,14 +223,20 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac | ||||
| /// and has successfully run recently (in the last six minutes) | ||||
| pub fn is_dns_updater_online() -> Result<bool, PeachError> { | ||||
|     // first check if it is enabled in peach-config | ||||
|     let is_enabled = get_dyndns_enabled_value()?; | ||||
|     let peach_config = load_peach_config()?; | ||||
|     let is_enabled = peach_config.dyn_enabled; | ||||
|     // then check if it has successfully run within the last 6 minutes (60*6 seconds) | ||||
|     let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?; | ||||
|     let ran_recently: bool = match num_seconds_since_successful_update { | ||||
|         Some(seconds) => seconds < (60 * 6), | ||||
|     let ran_recently: bool; | ||||
|     match num_seconds_since_successful_update { | ||||
|         Some(seconds) => { | ||||
|             ran_recently = seconds < (60 * 6); | ||||
|         } | ||||
|         // if the value is None, then the last time it ran successfully is unknown | ||||
|         None => false, | ||||
|     }; | ||||
|         None => { | ||||
|             ran_recently = false; | ||||
|         } | ||||
|     } | ||||
|     // debug log | ||||
|     info!("is_dyndns_enabled: {:?}", is_enabled); | ||||
|     info!("dyndns_ran_recently: {:?}", ran_recently); | ||||
| @ -245,9 +258,11 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> { | ||||
| } | ||||
|  | ||||
| // helper function which checks if a dyndns domain is new | ||||
| pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> { | ||||
|     let previous_dyndns_domain = get_config_value("DYN_DOMAIN")?; | ||||
|     Ok(dyndns_full_domain != previous_dyndns_domain) | ||||
| pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool { | ||||
|     // TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator | ||||
|     let peach_config = load_peach_config().unwrap(); | ||||
|     let previous_dyndns_domain = peach_config.dyn_domain; | ||||
|     dyndns_full_domain != previous_dyndns_domain | ||||
| } | ||||
|  | ||||
| jsonrpc_client!(pub struct PeachDynDnsClient { | ||||
|  | ||||
| @ -1,22 +1,12 @@ | ||||
| #![warn(missing_docs)] | ||||
|  | ||||
| //! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities. | ||||
|  | ||||
| use std::{io, str, string}; | ||||
| use jsonrpc_client::JsonRpcError; | ||||
| use anyhow::Error; // Add the anyhow crate for errors | ||||
|  | ||||
| /// This type represents all possible errors that can occur when interacting with the PeachCloud library. | ||||
| #[derive(Debug)] | ||||
| pub enum PeachError { | ||||
|     /// Represents looking up a Config value with a non-existent key | ||||
|     InvalidKey { | ||||
|         /// the key value which was invalid | ||||
|         key: String, | ||||
|     }, | ||||
|  | ||||
|     /// Represents a failure to determine the path of the user's home directory. | ||||
|     HomeDir, | ||||
|  | ||||
|     /// Represents all other cases of `std::io::Error`. | ||||
|     Io(io::Error), | ||||
|  | ||||
| @ -68,18 +58,15 @@ pub enum PeachError { | ||||
|     /// Represents a failure to parse or compile a regular expression. | ||||
|     Regex(regex::Error), | ||||
|  | ||||
|     /// Represents a failure to successfully execute an sbot command (via golgi). | ||||
|     Sbot(String), | ||||
|     /// Represents a failure to successfully execute an sbot command. | ||||
|     SbotCli { | ||||
|         /// The `stderr` output from the sbot command. | ||||
|         msg: String, | ||||
|     }, | ||||
|  | ||||
|     /// Represents a failure to serialize or deserialize JSON. | ||||
|     SerdeJson(serde_json::error::Error), | ||||
|  | ||||
|     /// Represents a failure to deserialize TOML. | ||||
|     TomlDeser(toml::de::Error), | ||||
|  | ||||
|     /// Represents a failure to serialize TOML. | ||||
|     TomlSer(toml::ser::Error), | ||||
|  | ||||
|     /// Represents a failure to serialize or deserialize YAML. | ||||
|     SerdeYaml(serde_yaml::Error), | ||||
|  | ||||
| @ -100,26 +87,14 @@ pub enum PeachError { | ||||
|     Write { | ||||
|         /// The underlying source of the error. | ||||
|         source: io::Error, | ||||
|         /// The file path for the write attempt. | ||||
|         /// The file path for the write attemp. | ||||
|         path: String, | ||||
|     }, | ||||
|  | ||||
|     /// Represents a JsonRpcError with Solar | ||||
|     JsonRpcError(JsonRpcError), | ||||
|  | ||||
|     /// Represents an Anyhow error with Solar | ||||
|     SolarClientError(String), | ||||
|  | ||||
|     /// Represents an error with encoding or decoding an SsbMessage | ||||
|     SsbMessageError(String), | ||||
|  | ||||
| } | ||||
|  | ||||
| impl std::error::Error for PeachError { | ||||
|     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | ||||
|         match *self { | ||||
|             PeachError::HomeDir => None, | ||||
|             PeachError::InvalidKey { .. } => None, | ||||
|             PeachError::Io(_) => None, | ||||
|             PeachError::JsonRpcClientCore(_) => None, | ||||
|             PeachError::JsonRpcCore(_) => None, | ||||
| @ -132,18 +107,13 @@ impl std::error::Error for PeachError { | ||||
|             PeachError::PasswordNotSet => None, | ||||
|             PeachError::Read { ref source, .. } => Some(source), | ||||
|             PeachError::Regex(_) => None, | ||||
|             PeachError::Sbot(_) => None, | ||||
|             PeachError::SbotCli { .. } => None, | ||||
|             PeachError::SerdeJson(_) => None, | ||||
|             PeachError::SerdeYaml(_) => None, | ||||
|             PeachError::SsbAdminIdNotFound { .. } => None, | ||||
|             PeachError::TomlDeser(_) => None, | ||||
|             PeachError::TomlSer(_) => None, | ||||
|             PeachError::Utf8ToStr(_) => None, | ||||
|             PeachError::Utf8ToString(_) => None, | ||||
|             PeachError::Write { ref source, .. } => Some(source), | ||||
|             PeachError::JsonRpcError(_) => None, | ||||
|             PeachError::SolarClientError(_) => None, | ||||
|             PeachError::SsbMessageError(_) => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -151,15 +121,6 @@ impl std::error::Error for PeachError { | ||||
| impl std::fmt::Display for PeachError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { | ||||
|         match *self { | ||||
|             PeachError::InvalidKey { ref key } => { | ||||
|                 write!(f, "Invalid key in config lookup for key: {}", key) | ||||
|             } | ||||
|             PeachError::HomeDir => { | ||||
|                 write!( | ||||
|                     f, | ||||
|                     "Unable to determine the path of the user's home directory" | ||||
|                 ) | ||||
|             } | ||||
|             PeachError::Io(ref err) => err.fmt(f), | ||||
|             PeachError::JsonRpcClientCore(ref err) => err.fmt(f), | ||||
|             PeachError::JsonRpcCore(ref err) => { | ||||
| @ -174,19 +135,22 @@ impl std::fmt::Display for PeachError { | ||||
|                 write!(f, "Date/time parse error: {}", path) | ||||
|             } | ||||
|             PeachError::PasswordIncorrect => { | ||||
|                 write!(f, "password is incorrect") | ||||
|                 write!(f, "Password error: user-supplied password is incorrect") | ||||
|             } | ||||
|             PeachError::PasswordMismatch => { | ||||
|                 write!(f, "passwords do not match") | ||||
|                 write!(f, "Password error: user-supplied passwords do not match") | ||||
|             } | ||||
|             PeachError::PasswordNotSet => { | ||||
|                 write!(f, "hash value in YAML configuration file is empty") | ||||
|                 write!( | ||||
|                     f, | ||||
|                     "Password error: hash value in YAML configuration file is empty" | ||||
|                 ) | ||||
|             } | ||||
|             PeachError::Read { ref path, .. } => { | ||||
|                 write!(f, "Read error: {}", path) | ||||
|             } | ||||
|             PeachError::Regex(ref err) => err.fmt(f), | ||||
|             PeachError::Sbot(ref msg) => { | ||||
|             PeachError::SbotCli { ref msg } => { | ||||
|                 write!(f, "Sbot error: {}", msg) | ||||
|             } | ||||
|             PeachError::SerdeJson(ref err) => err.fmt(f), | ||||
| @ -194,16 +158,11 @@ impl std::fmt::Display for PeachError { | ||||
|             PeachError::SsbAdminIdNotFound { ref id } => { | ||||
|                 write!(f, "Config error: SSB admin ID `{}` not found", id) | ||||
|             } | ||||
|             PeachError::TomlDeser(ref err) => err.fmt(f), | ||||
|             PeachError::TomlSer(ref err) => err.fmt(f), | ||||
|             PeachError::Utf8ToStr(ref err) => err.fmt(f), | ||||
|             PeachError::Utf8ToString(ref err) => err.fmt(f), | ||||
|             PeachError::Write { ref path, .. } => { | ||||
|                 write!(f, "Write error: {}", path) | ||||
|             } | ||||
|             PeachError::JsonRpcError(ref err) => err.fmt(f), | ||||
|             PeachError::SolarClientError(ref err) => err.fmt(f), | ||||
|             PeachError::SsbMessageError(ref err) => err.fmt(f), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -250,18 +209,6 @@ impl From<serde_yaml::Error> for PeachError { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<toml::de::Error> for PeachError { | ||||
|     fn from(err: toml::de::Error) -> PeachError { | ||||
|         PeachError::TomlDeser(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<toml::ser::Error> for PeachError { | ||||
|     fn from(err: toml::ser::Error) -> PeachError { | ||||
|         PeachError::TomlSer(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<str::Utf8Error> for PeachError { | ||||
|     fn from(err: str::Utf8Error) -> PeachError { | ||||
|         PeachError::Utf8ToStr(err) | ||||
| @ -273,16 +220,3 @@ impl From<string::FromUtf8Error> for PeachError { | ||||
|         PeachError::Utf8ToString(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<JsonRpcError> for PeachError { | ||||
|     fn from(err: JsonRpcError) -> PeachError { | ||||
|         PeachError::JsonRpcError(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<anyhow::Error> for PeachError { | ||||
|     fn from(error: anyhow::Error) -> Self { | ||||
|         // TODO: include whole error somehow? | ||||
|         PeachError::SolarClientError(error.to_string()) | ||||
|     } | ||||
| } | ||||
| @ -4,13 +4,11 @@ pub mod error; | ||||
| pub mod network_client; | ||||
| pub mod oled_client; | ||||
| pub mod password_utils; | ||||
| pub mod sbot; | ||||
| pub mod sbot_client; | ||||
| pub mod stats_client; | ||||
| pub mod ssb_messages; | ||||
|  | ||||
| // re-export error types | ||||
| pub use jsonrpc_client_core; | ||||
| pub use jsonrpc_core; | ||||
| pub use serde_json; | ||||
| pub use serde_yaml; | ||||
| pub use tilde_client; | ||||
|  | ||||
| @ -1,16 +1,13 @@ | ||||
| use async_std::task; | ||||
| use log::debug; | ||||
| use nanorand::{Rng, WyRand}; | ||||
| use sha3::{Digest, Sha3_256}; | ||||
|  | ||||
| use crate::sbot::init_sbot; | ||||
| use crate::{config_manager, error::PeachError, sbot::SbotConfig}; | ||||
| use crate::{config_manager, error::PeachError, sbot_client}; | ||||
|  | ||||
| /// Returns Ok(()) if the supplied password is correct, | ||||
| /// and returns Err if the supplied password is incorrect. | ||||
| pub fn verify_password(password: &str) -> Result<(), PeachError> { | ||||
|     let real_admin_password_hash = config_manager::get_admin_password_hash()?; | ||||
|     let password_hash = hash_password(password); | ||||
|     let password_hash = hash_password(&password.to_string()); | ||||
|     if real_admin_password_hash == password_hash { | ||||
|         Ok(()) | ||||
|     } else { | ||||
| @ -32,8 +29,8 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul | ||||
|  | ||||
| /// Sets a new password for the admin user | ||||
| pub fn set_new_password(new_password: &str) -> Result<(), PeachError> { | ||||
|     let new_password_hash = hash_password(new_password); | ||||
|     config_manager::set_admin_password_hash(new_password_hash)?; | ||||
|     let new_password_hash = hash_password(&new_password.to_string()); | ||||
|     config_manager::set_admin_password_hash(&new_password_hash)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| @ -52,8 +49,8 @@ pub fn hash_password(password: &str) -> String { | ||||
| /// Sets a new temporary password for the admin user | ||||
| /// which can be used to reset the permanent password | ||||
| pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> { | ||||
|     let new_password_hash = hash_password(new_password); | ||||
|     config_manager::set_temporary_password_hash(new_password_hash)?; | ||||
|     let new_password_hash = hash_password(&new_password.to_string()); | ||||
|     config_manager::set_temporary_password_hash(&new_password_hash)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| @ -62,7 +59,7 @@ pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> | ||||
| /// and returns Err if the supplied temp_password is incorrect | ||||
| pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> { | ||||
|     let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?; | ||||
|     let password_hash = hash_password(password); | ||||
|     let password_hash = hash_password(&password.to_string()); | ||||
|     if temporary_admin_password_hash == password_hash { | ||||
|         Ok(()) | ||||
|     } else { | ||||
| @ -86,7 +83,7 @@ pub fn send_password_reset() -> Result<(), PeachError> { | ||||
|         "Your new temporary password is: {} | ||||
|  | ||||
| If you are on the same WiFi network as your PeachCloud device you can reset your password \ | ||||
| using this link: http://peach.local/auth/reset", | ||||
| using this link: http://peach.local/reset_password", | ||||
|         temporary_password | ||||
|     ); | ||||
|     // if there is an external domain, then include remote link in message | ||||
| @ -95,7 +92,7 @@ using this link: http://peach.local/auth/reset", | ||||
|         Some(domain) => { | ||||
|             format!( | ||||
|                 "\n\nOr if you are on a different WiFi network, you can reset your password \ | ||||
|             using the the following link: {}/auth/reset", | ||||
|             using the the following link: {}/reset_password", | ||||
|                 domain | ||||
|             ) | ||||
|         } | ||||
| @ -103,34 +100,9 @@ using this link: http://peach.local/auth/reset", | ||||
|     }; | ||||
|     msg += &remote_link; | ||||
|     // finally send the message to the admins | ||||
|     let ssb_admin_ids = config_manager::get_ssb_admin_ids()?; | ||||
|     for ssb_admin_id in ssb_admin_ids { | ||||
|         // use golgi to send a private message on scuttlebutt | ||||
|         match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) { | ||||
|             Ok(_) => (), | ||||
|             Err(e) => return Err(PeachError::Sbot(e)), | ||||
|         } | ||||
|     let peach_config = config_manager::load_peach_config()?; | ||||
|     for ssb_admin_id in peach_config.ssb_admin_ids { | ||||
|         sbot_client::private_message(&msg, &ssb_admin_id)?; | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> { | ||||
|     // retrieve latest go-sbot configuration parameters | ||||
|     let sbot_config = SbotConfig::read().ok(); | ||||
|  | ||||
|     let msg = msg.to_string(); | ||||
|     let recipient = vec![recipient.to_string()]; | ||||
|  | ||||
|     // initialise sbot connection with ip:port and shscap from config file | ||||
|     let mut sbot_client = init_sbot(); | ||||
|  | ||||
|     debug!("Publishing a Scuttlebutt private message with temporary password"); | ||||
|     // TODO: implement publish private message in solar, and then implement this | ||||
|     Err(format!("Failed to publish private message: \ | ||||
|         private publishing is not yet implemented in solar_client: \ | ||||
|         the message meant to be sent was: {}", msg)) | ||||
|     // match sbot_client.publish_private(msg, recipient).await { | ||||
|     //     Ok(_) => Ok(()), | ||||
|     //     Err(e) => Err(format!("Failed to publish private message: {}", e)), | ||||
|     // } | ||||
| } | ||||
|  | ||||
| @ -1,288 +0,0 @@ | ||||
| //! Data types and associated methods for monitoring and configuring solar-sbot. | ||||
|  | ||||
| use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str}; | ||||
| use std::os::linux::raw::ino_t; | ||||
| use tilde_client::{TildeClient, get_sbot_client}; | ||||
| use log::debug; | ||||
|  | ||||
| use crate::config_manager; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::error::PeachError; | ||||
|  | ||||
| /* HELPER FUNCTIONS */ | ||||
|  | ||||
| // iterate over the given directory path to determine the size of the directory | ||||
| fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> { | ||||
|     fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> { | ||||
|         dir.try_fold(0, |acc, file| { | ||||
|             let file = file?; | ||||
|             let size = match file.metadata()? { | ||||
|                 data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?, | ||||
|                 data => data.len(), | ||||
|             }; | ||||
|             Ok(acc + size) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     dir_size(fs::read_dir(path.into())?) | ||||
| } | ||||
|  | ||||
| /* SBOT-RELATED TYPES AND METHODS */ | ||||
|  | ||||
| /// solar-sbot process status. | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct SbotStatus { | ||||
|     /// Current process state. | ||||
|     pub state: Option<String>, | ||||
|     /// Current process boot state. | ||||
|     pub boot_state: Option<String>, | ||||
|     /// Current process memory usage in bytes. | ||||
|     pub memory: Option<u32>, | ||||
|     /// Uptime for the process (if state is `active`). | ||||
|     pub uptime: Option<String>, | ||||
|     /// Downtime for the process (if state is `inactive`). | ||||
|     pub downtime: Option<String>, | ||||
|     /// Size of the blobs directory in bytes. | ||||
|     pub blobstore: Option<u64>, | ||||
| } | ||||
|  | ||||
| /// Default builder for `SbotStatus`. | ||||
| impl Default for SbotStatus { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             state: None, | ||||
|             boot_state: None, | ||||
|             memory: None, | ||||
|             uptime: None, | ||||
|             downtime: None, | ||||
|             blobstore: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl SbotStatus { | ||||
|     /// Retrieve statistics for the solar-sbot systemd process by querying `systemctl`. | ||||
|     pub fn read() -> Result<Self, PeachError> { | ||||
|         let mut status = SbotStatus::default(); | ||||
|  | ||||
|         // note this command does not need to be run as sudo | ||||
|         // because non-privileged users are able to run systemctl show | ||||
|         let service_name = config_manager::get_config_value("TILDE_SBOT_SERVICE")?; | ||||
|         let info_output = Command::new("systemctl") | ||||
|             .arg("show") | ||||
|             .arg(service_name) | ||||
|             .arg("--no-page") | ||||
|             .output()?; | ||||
|  | ||||
|         let service_info = std::str::from_utf8(&info_output.stdout)?; | ||||
|  | ||||
|         for line in service_info.lines() { | ||||
|             if line.starts_with("ActiveState=") { | ||||
|                 if let Some(state) = line.strip_prefix("ActiveState=") { | ||||
|                     status.state = Some(state.to_string()) | ||||
|                 } | ||||
|             } else if line.starts_with("MemoryCurrent=") { | ||||
|                 if let Some(memory) = line.strip_prefix("MemoryCurrent=") { | ||||
|                     status.memory = memory.parse().ok() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // note this command does not need to be run as sudo | ||||
|         // because non-privileged users are able to run systemctl status | ||||
|         let status_output = Command::new("systemctl") | ||||
|             .arg("status") | ||||
|             .arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?) | ||||
|             .output()?; | ||||
|  | ||||
|         let service_status = str::from_utf8(&status_output.stdout)?; | ||||
|         //.map_err(PeachError::Utf8ToStr)?; | ||||
|  | ||||
|         for line in service_status.lines() { | ||||
|             // example of the output line we're looking for: | ||||
|             // `Loaded: loaded (/home/glyph/.config/systemd/user/solar-sbot.service; enabled; vendor | ||||
|             // preset: enabled)` | ||||
|             if line.contains("Loaded:") { | ||||
|                 let before_boot_state = line.find(';'); | ||||
|                 let after_boot_state = line.rfind(';'); | ||||
|                 if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) { | ||||
|                     // extract the enabled / disabled from the `Loaded: ...` line | ||||
|                     // using the index of the first ';' + 2 and the last ';' | ||||
|                     status.boot_state = Some(line[start + 2..end].to_string()); | ||||
|                 } | ||||
|             // example of the output line we're looking for here: | ||||
|             // `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago` | ||||
|             } else if line.contains("Active:") { | ||||
|                 let before_time = line.find(';'); | ||||
|                 let after_time = line.find(" ago"); | ||||
|                 if let (Some(start), Some(end)) = (before_time, after_time) { | ||||
|                     // extract the uptime / downtime from the `Active: ...` line | ||||
|                     // using the index of ';' + 2 and the index of " ago" | ||||
|                     let time = Some(&line[start + 2..end]); | ||||
|                     // if service is active then the `time` reading is uptime | ||||
|                     if status.state == Some("active".to_string()) { | ||||
|                         status.uptime = time.map(|t| t.to_string()) | ||||
|                     // if service is inactive then the `time` reading is downtime | ||||
|                     } else if status.state == Some("inactive".to_string()) { | ||||
|                         status.downtime = time.map(|t| t.to_string()) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // TOOD restore this | ||||
|         // get path to blobstore | ||||
|         // let blobstore_path = format!( | ||||
|         //     "{}/blobs/sha256", | ||||
|         //     config_manager::get_config_value("TILDE_SBOT_DATADIR")? | ||||
|         // ); | ||||
|         let blobstore_path = format!( | ||||
|             "{}", | ||||
|             config_manager::get_config_value("TILDE_SBOT_DATADIR")? | ||||
|         ); | ||||
|  | ||||
|         // determine the size of the blobstore directory in bytes | ||||
|         status.blobstore = dir_size(blobstore_path).ok(); | ||||
|  | ||||
|         Ok(status) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// solar-sbot configuration parameters. | ||||
| #[derive(Debug, Serialize, Deserialize, Default)] | ||||
| #[serde(default)] | ||||
| pub struct Config { | ||||
|     // TODO: maybe define as a Path type? | ||||
|     /// Directory path for the log and indexes. | ||||
|     pub repo: String, | ||||
|     /// Directory path for writing debug output. | ||||
|     pub debugdir: String, | ||||
|     /// Secret-handshake app-key (aka. network key). | ||||
|     pub shscap: String, | ||||
|     /// HMAC hash used to sign messages. | ||||
|     pub hmac: String, | ||||
|     /// Replication hops (1: friends, 2: friends of friends). | ||||
|     pub hops: u8, | ||||
|     /// Address to listen on. | ||||
|     pub lis: String, | ||||
|     /// Address to listen on for WebSocket connections. | ||||
|     pub wslis: String, | ||||
|     /// Address to for metrics and pprof HTTP server. | ||||
|     pub debuglis: String, | ||||
|     /// Enable sending local UDP broadcasts. | ||||
|     pub localadv: bool, | ||||
|     /// Enable listening for UDP broadcasts and connecting. | ||||
|     pub localdiscov: bool, | ||||
|     /// Enable syncing by using epidemic-broadcast-trees (EBT). | ||||
|     #[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))] | ||||
|     pub enable_ebt: bool, | ||||
|     /// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data | ||||
|     /// from peer; user beware - caveats about). | ||||
|     pub promisc: bool, | ||||
|     /// Disable the UNIX socket RPC interface. | ||||
|     pub nounixsock: bool, | ||||
|     /// Attempt to repair the filesystem before starting. | ||||
|     pub repair: bool, | ||||
| } | ||||
|  | ||||
| // TODO: make this real | ||||
| #[derive(Debug, Deserialize, Serialize)] | ||||
| #[serde(default)] | ||||
| pub struct SbotConfig { | ||||
|     pub repo: String, | ||||
|     pub debugdir: String, | ||||
|     pub shscap: String, | ||||
|     pub hmac: String, | ||||
|     pub hops: i8, | ||||
|     pub lis: String, | ||||
|     pub wslis: String, | ||||
|     pub debuglis: String, | ||||
|     pub localadv: bool, | ||||
|     pub localdiscov: bool, | ||||
|     pub enable_ebt: bool, | ||||
|     pub promisc: bool, | ||||
|     pub nounixsock: bool, | ||||
|     pub repair: bool, | ||||
| } | ||||
|  | ||||
| /// Default configuration values for solar-sbot. | ||||
| impl Default for SbotConfig { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             repo: ".ssb-go".to_string(), | ||||
|             debugdir: "".to_string(), | ||||
|             shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(), | ||||
|             hmac: "".to_string(), | ||||
|             hops: 1, | ||||
|             lis: ":8008".to_string(), | ||||
|             wslis: ":8989".to_string(), | ||||
|             debuglis: "localhost:6078".to_string(), | ||||
|             localadv: false, | ||||
|             localdiscov: false, | ||||
|             enable_ebt: false, | ||||
|             promisc: false, | ||||
|             nounixsock: false, | ||||
|             repair: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl SbotConfig { | ||||
|     /// Read the solar-sbot `config.toml` file from file and deserialize into `SbotConfig`. | ||||
|     pub fn read() -> Result<Self, PeachError> { | ||||
|         // determine path of user's solar-sbot config.toml | ||||
|         let config_path = format!( | ||||
|             "{}/config.toml", | ||||
|             config_manager::get_config_value("SOLAR_SBOT_DATADIR")? | ||||
|         ); | ||||
|  | ||||
|         let config_contents = fs::read_to_string(config_path)?; | ||||
|  | ||||
|         let config: SbotConfig = toml::from_str(&config_contents)?; | ||||
|  | ||||
|         Ok(config) | ||||
|     } | ||||
|  | ||||
|     /// Write the given `SbotConfig` to the solar-sbot `config.toml` file. | ||||
|     pub fn write(config: SbotConfig) -> Result<(), PeachError> { | ||||
|         let repo_comment = "# For details about solar-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string(); | ||||
|  | ||||
|         // convert the provided `SbotConfig` instance to a string | ||||
|         let config_string = toml::to_string(&config)?; | ||||
|  | ||||
|         // determine path of user's solar-sbot config.toml | ||||
|         let config_path = format!( | ||||
|             "{}/config.toml", | ||||
|             config_manager::get_config_value("SOLAR_SBOT_DATADIR")? | ||||
|         ); | ||||
|  | ||||
|         // open config file for writing | ||||
|         let mut file = File::create(config_path)?; | ||||
|  | ||||
|         // write the repo comment to file | ||||
|         write!(file, "{}", repo_comment)?; | ||||
|  | ||||
|         // write the config string to file | ||||
|         write!(file, "{}", config_string)?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Initialise an sbot client | ||||
| pub async fn init_sbot() -> Result<TildeClient, PeachError> { | ||||
|     // read sbot config from config.toml | ||||
|     let sbot_config = SbotConfig::read().ok(); | ||||
|  | ||||
|     debug!("Initialising an sbot client with configuration parameters"); | ||||
|     // initialise sbot connection with ip:port and shscap from config file | ||||
|     let key_path = format!( | ||||
|         "{}/secret", | ||||
|         config_manager::get_config_value("SOLAR_SBOT_DATADIR")? | ||||
|     ); | ||||
|     // TODO: read this from config | ||||
|     const SERVER_ADDR: &str = "http://127.0.0.1:3030"; | ||||
|     let sbot_client = get_sbot_client(); | ||||
|     Ok(sbot_client) | ||||
| } | ||||
							
								
								
									
										111
									
								
								peach-lib/src/sbot_client.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,111 @@ | ||||
| //! Interfaces for monitoring and configuring go-sbot using sbotcli. | ||||
|  | ||||
| use std::process::Command; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::error::PeachError; | ||||
|  | ||||
| pub fn is_sbot_online() -> Result<bool, PeachError> { | ||||
|     let output = Command::new("/usr/bin/systemctl") | ||||
|         .arg("status") | ||||
|         .arg("peach-go-sbot") | ||||
|         .output()?; | ||||
|     let status = output.status; | ||||
|     // returns true if the service had an exist status of 0 (is running) | ||||
|     let is_running = status.success(); | ||||
|     Ok(is_running) | ||||
| } | ||||
|  | ||||
| /// currently go-sbotcli determines where the working directory is | ||||
| /// using the home directory of th user that invokes it | ||||
| /// this could be changed to be supplied as CLI arg | ||||
| /// but for now all sbotcli commands must first become peach-go-sbot before running | ||||
| /// the sudoers file is configured to allow this to happen without a password | ||||
| pub fn sbotcli_command() -> Command { | ||||
|     let mut command = Command::new("sudo"); | ||||
|     command | ||||
|         .arg("-u") | ||||
|         .arg("peach-go-sbot") | ||||
|         .arg("/usr/bin/sbotcli"); | ||||
|     command | ||||
| } | ||||
|  | ||||
| pub fn post(msg: &str) -> Result<(), PeachError> { | ||||
|     let mut command = sbotcli_command(); | ||||
|     let output = command.arg("publish").arg("post").arg(msg).output()?; | ||||
|     if output.status.success() { | ||||
|         Ok(()) | ||||
|     } else { | ||||
|         let stderr = std::str::from_utf8(&output.stderr)?; | ||||
|         Err(PeachError::SbotCli { | ||||
|             msg: format!("Error making ssb post: {}", stderr), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize)] | ||||
| struct WhoAmIValue { | ||||
|     id: String, | ||||
| } | ||||
|  | ||||
| pub fn whoami() -> Result<String, PeachError> { | ||||
|     let mut command = sbotcli_command(); | ||||
|     let output = command.arg("call").arg("whoami").output()?; | ||||
|     let text_output = std::str::from_utf8(&output.stdout)?; | ||||
|     let value: WhoAmIValue = serde_json::from_str(text_output)?; | ||||
|     let id = value.id; | ||||
|     Ok(id) | ||||
| } | ||||
|  | ||||
| pub fn create_invite(uses: i32) -> Result<String, PeachError> { | ||||
|     let mut command = sbotcli_command(); | ||||
|     let output = command | ||||
|         .arg("invite") | ||||
|         .arg("create") | ||||
|         .arg("--uses") | ||||
|         .arg(uses.to_string()) | ||||
|         .output()?; | ||||
|     let text_output = std::str::from_utf8(&output.stdout)?; | ||||
|     let output = text_output.replace("\n", ""); | ||||
|     Ok(output) | ||||
| } | ||||
|  | ||||
| pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> { | ||||
|     let pub_ssb_id = whoami()?; | ||||
|     let mut command = sbotcli_command(); | ||||
|     let output = command | ||||
|         .arg("publish") | ||||
|         .arg("about") | ||||
|         .arg("--name") | ||||
|         .arg(new_name) | ||||
|         .arg(pub_ssb_id) | ||||
|         .output()?; | ||||
|     if output.status.success() { | ||||
|         Ok(()) | ||||
|     } else { | ||||
|         let stderr = std::str::from_utf8(&output.stderr)?; | ||||
|         Err(PeachError::SbotCli { | ||||
|             msg: format!("Error updating pub name: {}", stderr), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> { | ||||
|     let mut command = sbotcli_command(); | ||||
|     let output = command | ||||
|         .arg("publish") | ||||
|         .arg("post") | ||||
|         .arg("--recps") | ||||
|         .arg(recipient) | ||||
|         .arg(msg) | ||||
|         .output()?; | ||||
|     if output.status.success() { | ||||
|         Ok(()) | ||||
|     } else { | ||||
|         let stderr = std::str::from_utf8(&output.stderr)?; | ||||
|         Err(PeachError::SbotCli { | ||||
|             msg: format!("Error sending ssb private message: {}", stderr), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @ -1,104 +0,0 @@ | ||||
| //! Message types and conversion methods. | ||||
|  | ||||
| use kuska_ssb::api::dto::content::TypedMessage; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json::Value; | ||||
| use std::fmt::Debug; | ||||
| use crate::error::PeachError; | ||||
| use crate::error::PeachError::SsbMessageError; | ||||
|  | ||||
| /// `SsbMessageContent` is a type alias for `TypedMessage` from the `kuska_ssb` library. | ||||
| /// It is aliased in golgi to fit the naming convention of the other message | ||||
| /// types: `SsbMessageKVT` and `SsbMessageValue`. | ||||
| /// | ||||
| /// See the [kuska source code](https://github.com/Kuska-ssb/ssb/blob/master/src/api/dto/content.rs#L103) for the type definition of `TypedMessage`. | ||||
| pub type SsbMessageContent = TypedMessage; | ||||
|  | ||||
| /// The `value` of an SSB message (the `V` in `KVT`). | ||||
| /// | ||||
| /// More information concerning the data model can be found in the | ||||
| /// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata). | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| #[serde(deny_unknown_fields)] | ||||
| #[allow(missing_docs)] | ||||
| pub struct SsbMessageValue { | ||||
|     pub previous: Option<String>, | ||||
|     pub author: String, | ||||
|     pub sequence: u64, | ||||
|     pub timestamp: f64, | ||||
|     pub hash: String, | ||||
|     pub content: Value, | ||||
|     pub signature: String, | ||||
| } | ||||
|  | ||||
| /// Message content types. | ||||
| #[derive(Debug, Eq, PartialEq)] | ||||
| #[allow(missing_docs)] | ||||
| pub enum SsbMessageContentType { | ||||
|     About, | ||||
|     Vote, | ||||
|     Post, | ||||
|     Contact, | ||||
|     Unrecognized, | ||||
| } | ||||
|  | ||||
| impl SsbMessageValue { | ||||
|     /// Get the type field of the message content as an enum, if found. | ||||
|     /// | ||||
|     /// If no `type` field is found or the `type` field is not a string, | ||||
|     /// it returns an `Err(GolgiError::ContentType)`. | ||||
|     /// | ||||
|     /// If a `type` field is found but with an unknown string, | ||||
|     /// it returns an `Ok(SsbMessageContentType::Unrecognized)`. | ||||
|     pub fn get_message_type(&self) -> Result<SsbMessageContentType, PeachError> { | ||||
|         let msg_type = self | ||||
|             .content | ||||
|             .get("type") | ||||
|             .ok_or_else(|| SsbMessageError("type field not found".to_string()))?; | ||||
|         let mtype_str: &str = msg_type.as_str().ok_or_else(|| { | ||||
|             SsbMessageError("type field value is not a string as expected".to_string()) | ||||
|         })?; | ||||
|         let enum_type = match mtype_str { | ||||
|             "about" => SsbMessageContentType::About, | ||||
|             "post" => SsbMessageContentType::Post, | ||||
|             "vote" => SsbMessageContentType::Vote, | ||||
|             "contact" => SsbMessageContentType::Contact, | ||||
|             _ => SsbMessageContentType::Unrecognized, | ||||
|         }; | ||||
|         Ok(enum_type) | ||||
|     } | ||||
|  | ||||
|     /// Helper function which returns `true` if this message is of the given type, | ||||
|     /// and `false` if the type does not match or is not found. | ||||
|     pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool { | ||||
|         let self_message_type = self.get_message_type(); | ||||
|         match self_message_type { | ||||
|             Ok(mtype) => mtype == message_type, | ||||
|             Err(_err) => false, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Convert the content JSON value into an `SsbMessageContent` `enum`, | ||||
|     /// using the `type` field as a tag to select which variant of the `enum` | ||||
|     /// to deserialize into. | ||||
|     /// | ||||
|     /// See the [Serde docs on internally-tagged enum representations](https://serde.rs/enum-representations.html#internally-tagged) for further details. | ||||
|     pub fn into_ssb_message_content(self) -> Result<SsbMessageContent, PeachError> { | ||||
|         let m: SsbMessageContent = serde_json::from_value(self.content)?; | ||||
|         Ok(m) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// An SSB message represented as a key-value-timestamp (`KVT`). | ||||
| /// | ||||
| /// More information concerning the data model can be found in the | ||||
| /// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata). | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| #[serde(deny_unknown_fields)] | ||||
| #[allow(missing_docs)] | ||||
| pub struct SsbMessageKVT { | ||||
|     pub key: String, | ||||
|     pub value: SsbMessageValue, | ||||
|     pub timestamp: Option<f64>, | ||||
|     pub rts: Option<f64>, | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "peach-network" | ||||
| version = "0.5.0" | ||||
| version = "0.4.1" | ||||
| authors = ["Andrew Reid <glyph@mycelial.technology>"] | ||||
| edition = "2021" | ||||
| description = "Query and configure network interfaces." | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| # peach-network | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| Network interface state query and modification library. | ||||
|  | ||||
|  | ||||
| @ -6,7 +6,7 @@ use std::num::ParseIntError; | ||||
| use io::Error as IoError; | ||||
| use probes::ProbeError; | ||||
| use regex::Error as RegexError; | ||||
| use wpactrl::Error as WpaError; | ||||
| use wpactrl::WpaError; | ||||
|  | ||||
| /// Custom error type encapsulating all possible errors when querying | ||||
| /// network interfaces and modifying their state. | ||||
|  | ||||
| @ -14,7 +14,6 @@ | ||||
| //! access point credentials to `wpa_supplicant-<wlan_iface>.conf`. | ||||
|  | ||||
| use std::{ | ||||
|     collections::HashMap, | ||||
|     fs::OpenOptions, | ||||
|     io::prelude::*, | ||||
|     process::{Command, Stdio}, | ||||
| @ -23,7 +22,6 @@ use std::{ | ||||
| }; | ||||
|  | ||||
| use probes::network; | ||||
| use wpactrl::Client as WpaClient; | ||||
|  | ||||
| #[cfg(feature = "miniserde_support")] | ||||
| use miniserde::{Deserialize, Serialize}; | ||||
| @ -107,86 +105,8 @@ pub struct Traffic { | ||||
|     pub transmitted: u64, | ||||
| } | ||||
|  | ||||
| /// Access point data including state and signal strength. | ||||
| #[derive(Debug)] | ||||
| #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] | ||||
| #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] | ||||
| pub struct AccessPoint { | ||||
|     /// Access point data retrieved via scan. | ||||
|     pub detail: Option<Scan>, | ||||
|     /// Current state of the access point (e.g. "Available" or "Out of range"). | ||||
|     pub state: String, | ||||
|     /// Signal strength of the access point as a percentage. | ||||
|     pub signal: Option<i32>, | ||||
| } | ||||
|  | ||||
| impl AccessPoint { | ||||
|     fn available(detail: Option<Scan>, signal: Option<i32>) -> AccessPoint { | ||||
|         AccessPoint { | ||||
|             detail, | ||||
|             state: String::from("Available"), | ||||
|             signal, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn saved() -> AccessPoint { | ||||
|         AccessPoint { | ||||
|             detail: None, | ||||
|             state: String::from("Out of range"), | ||||
|             signal: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* GET - Methods for retrieving data */ | ||||
|  | ||||
| /// Retrieve combined list of available (in-range) and saved wireless access | ||||
| /// points for a given network interface. | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `iface` - A string slice holding the name of a wireless network interface | ||||
| /// | ||||
| /// If the list results include one or more access points for the given network | ||||
| /// interface, an `Ok` `Result` type is returned containing `HashMap<String, | ||||
| /// AccessPoint>`. | ||||
| /// | ||||
| /// Each entry in the returned `HashMap` contains an SSID (`String`) and | ||||
| /// `AccessPoint` `struct`. If no access points are found, an empty `HashMap` | ||||
| /// is returned in the `Result`. In the event of an error, a `NetworkError` | ||||
| /// is returned in the `Result`. | ||||
| pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, NetworkError> { | ||||
|     let mut wlan_networks = HashMap::new(); | ||||
|  | ||||
|     if let Ok(Some(networks)) = available_networks(iface) { | ||||
|         for ap in networks { | ||||
|             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::available(Some(ap), Some(quality_percent)); | ||||
|             wlan_networks.insert(ssid, ap_detail); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if let Ok(Some(networks)) = saved_networks() { | ||||
|         for saved_ssid in networks { | ||||
|             if !wlan_networks.contains_key(&saved_ssid) { | ||||
|                 let ssid = saved_ssid.clone(); | ||||
|  | ||||
|                 let ap_detail = AccessPoint::saved(); | ||||
|                 wlan_networks.insert(ssid, ap_detail); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(wlan_networks) | ||||
| } | ||||
|  | ||||
| /// Retrieve list of available wireless access points for a given network | ||||
| /// interface. | ||||
| /// | ||||
| @ -201,7 +121,7 @@ pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, Network | ||||
| /// In the event of an error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     wpa.request("SCAN")?; | ||||
|     let networks = wpa.request("SCAN_RESULTS")?; | ||||
|     let mut scan = Vec::new(); | ||||
| @ -218,7 +138,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError | ||||
|             // we only want to return the auth / crypto flags | ||||
|             if flags_vec[0] != "[ESS]" { | ||||
|                 // parse auth / crypto flag and assign it to protocol | ||||
|                 protocol.push_str(flags_vec[0].replace('[', "").replace(']', "").as_str()); | ||||
|                 protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str()); | ||||
|             } | ||||
|             let ssid = v[4].to_string(); | ||||
|             let response = Scan { | ||||
| @ -253,7 +173,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError | ||||
| /// event of an error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let networks = wpa.request("LIST_NETWORKS")?; | ||||
|     let mut id = Vec::new(); | ||||
|     for network in networks.lines() { | ||||
| @ -312,7 +232,7 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
| /// `Result`. | ||||
| pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let status = wpa.request("SIGNAL_POLL")?; | ||||
|     let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; | ||||
|  | ||||
| @ -339,7 +259,7 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
| /// the `Result`. | ||||
| pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let status = wpa.request("SIGNAL_POLL")?; | ||||
|     let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; | ||||
|  | ||||
| @ -371,7 +291,7 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
| /// is returned in the `Result`. In the event of an error, a `NetworkError` is | ||||
| /// returned in the `Result`. | ||||
| pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> { | ||||
|     let mut wpa = WpaClient::builder().open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().open()?; | ||||
|     let networks = wpa.request("LIST_NETWORKS")?; | ||||
|     let mut ssids = Vec::new(); | ||||
|     for network in networks.lines() { | ||||
| @ -403,7 +323,7 @@ pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> { | ||||
| /// returned in the `Result`. | ||||
| pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let status = wpa.request("STATUS")?; | ||||
|  | ||||
|     // pass the regex pattern and status output to the regex finder | ||||
| @ -459,7 +379,7 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> { | ||||
| /// a `NetworkError` is returned in the `Result`. | ||||
| pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let wpa_status = wpa.request("STATUS")?; | ||||
|  | ||||
|     // pass the regex pattern and status output to the regex finder | ||||
| @ -659,7 +579,7 @@ pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError> | ||||
| /// is returned in the `Result`. | ||||
| pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let select = format!("SELECT {}", id); | ||||
|     wpa.request(&select)?; | ||||
|     Ok(()) | ||||
| @ -678,7 +598,7 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> { | ||||
| /// returned in the `Result`. | ||||
| pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let remove = format!("REMOVE_NETWORK {}", id); | ||||
|     wpa.request(&remove)?; | ||||
|     Ok(()) | ||||
| @ -697,7 +617,7 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> { | ||||
| /// `Result`. | ||||
| pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let disable = format!("DISABLE_NETWORK {}", id); | ||||
|     wpa.request(&disable)?; | ||||
|     Ok(()) | ||||
| @ -714,7 +634,7 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> { | ||||
| /// error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn disconnect(iface: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let disconnect = "DISCONNECT".to_string(); | ||||
|     wpa.request(&disconnect)?; | ||||
|     Ok(()) | ||||
| @ -765,7 +685,7 @@ pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> { | ||||
| /// event of an error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     let new_pass = format!("NEW_PASSWORD {} {}", id, pass); | ||||
|     wpa.request(&new_pass)?; | ||||
|     Ok(()) | ||||
| @ -782,7 +702,7 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> { | ||||
| /// error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn reassociate(iface: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     wpa.request("REASSOCIATE")?; | ||||
|     Ok(()) | ||||
| } | ||||
| @ -794,7 +714,7 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> { | ||||
| /// `Result` type is returned. In the event of an error, a `NetworkError` is | ||||
| /// returned in the `Result`. | ||||
| pub fn reconfigure() -> Result<(), NetworkError> { | ||||
|     let mut wpa = WpaClient::builder().open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().open()?; | ||||
|     wpa.request("RECONFIGURE")?; | ||||
|     Ok(()) | ||||
| } | ||||
| @ -810,7 +730,7 @@ pub fn reconfigure() -> Result<(), NetworkError> { | ||||
| /// event of an error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn reconnect(iface: &str) -> Result<(), NetworkError> { | ||||
|     let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); | ||||
|     let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?; | ||||
|     wpa.request("DISCONNECT")?; | ||||
|     wpa.request("RECONNECT")?; | ||||
|     Ok(()) | ||||
| @ -822,7 +742,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> { | ||||
| /// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the | ||||
| /// event of an error, a `NetworkError` is returned in the `Result`. | ||||
| pub fn save() -> Result<(), NetworkError> { | ||||
|     let mut wpa = WpaClient::builder().open()?; | ||||
|     let mut wpa = wpactrl::WpaCtrl::builder().open()?; | ||||
|     wpa.request("SAVE_CONFIG")?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "peach-stats" | ||||
| version = "0.3.1" | ||||
| version = "0.2.0" | ||||
| authors = ["Andrew Reid <glyph@mycelial.technology>"] | ||||
| edition = "2018" | ||||
| description = "Query system statistics. Provides a wrapper around the probes and systemstat crates." | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| # peach-stats | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates. | ||||
|  | ||||
| Currently offers the following system statistics and associated data structures: | ||||
| Currently offers the following statistics and associated data structures: | ||||
|  | ||||
|  - CPU: `user`, `system`, `nice`, `idle` (as values or percentages) | ||||
|  - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`, | ||||
| @ -13,14 +13,10 @@ Currently offers the following system statistics and associated data structures: | ||||
|  - Memory: `total`, `free`, `used` | ||||
|  - Uptime: `seconds` | ||||
|  | ||||
| As well as the following go-sbot process statistics: | ||||
|  | ||||
|  - Sbot: `state`, `memory`, `uptime`, `downtime` | ||||
|  | ||||
| ## Example Usage | ||||
|  | ||||
| ```rust | ||||
| use peach_stats::{sbot, stats, StatsError}; | ||||
| use peach_stats::{stats, StatsError}; | ||||
|  | ||||
| fn main() -> Result<(), StatsError> { | ||||
|     let cpu = stats::cpu_stats()?; | ||||
| @ -29,7 +25,6 @@ fn main() -> Result<(), StatsError> { | ||||
|     let load = stats::load_average()?; | ||||
|     let mem = stats::mem_stats()?; | ||||
|     let uptime = stats::uptime()?; | ||||
|     let sbot_process = sbot::sbot_stats()?; | ||||
|  | ||||
|     // do things with the retrieved values... | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| //! Custom error type for `peach-stats`. | ||||
|  | ||||
| use probes::ProbeError; | ||||
| use std::{error, fmt, io::Error as IoError, str::Utf8Error}; | ||||
| use std::{error, fmt, io::Error as IoError}; | ||||
|  | ||||
| /// Custom error type encapsulating all possible errors when retrieving system | ||||
| /// statistics. | ||||
| @ -17,10 +17,6 @@ pub enum StatsError { | ||||
|     MemStat(ProbeError), | ||||
|     /// Failed to retrieve system uptime. | ||||
|     Uptime(IoError), | ||||
|     /// Systemctl command returned an error. | ||||
|     Systemctl(IoError), | ||||
|     /// Failed to interpret sequence of `u8` as a string. | ||||
|     Utf8String(Utf8Error), | ||||
| } | ||||
|  | ||||
| impl error::Error for StatsError {} | ||||
| @ -43,12 +39,6 @@ impl fmt::Display for StatsError { | ||||
|             StatsError::Uptime(ref source) => { | ||||
|                 write!(f, "Failed to retrieve system uptime: {}", source) | ||||
|             } | ||||
|             StatsError::Systemctl(ref source) => { | ||||
|                 write!(f, "Systemctl command returned an error: {}", source) | ||||
|             } | ||||
|             StatsError::Utf8String(ref source) => { | ||||
|                 write!(f, "Failed to convert stdout to string: {}", source) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -43,7 +43,6 @@ | ||||
| //! ``` | ||||
|  | ||||
| pub mod error; | ||||
| pub mod sbot; | ||||
| pub mod stats; | ||||
|  | ||||
| pub use crate::error::StatsError; | ||||
|  | ||||
| @ -1,111 +0,0 @@ | ||||
| //! Systemd go-sbot process statistics retrieval functions and associated data types. | ||||
|  | ||||
| use std::{process::Command, str}; | ||||
|  | ||||
| #[cfg(feature = "miniserde_support")] | ||||
| use miniserde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[cfg(feature = "serde_support")] | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::StatsError; | ||||
|  | ||||
| /// go-sbot process statistics. | ||||
| #[derive(Debug)] | ||||
| #[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] | ||||
| #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] | ||||
| pub struct SbotStat { | ||||
|     /// Current process state. | ||||
|     pub state: Option<String>, | ||||
|     /// Current process boot state. | ||||
|     pub boot_state: Option<String>, | ||||
|     /// Current process memory usage in bytes. | ||||
|     pub memory: Option<u32>, | ||||
|     /// Uptime for the process (if state is `active`). | ||||
|     pub uptime: Option<String>, | ||||
|     /// Downtime for the process (if state is `inactive`). | ||||
|     pub downtime: Option<String>, | ||||
| } | ||||
|  | ||||
| impl SbotStat { | ||||
|     /// Default builder for `SbotStat`. | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             state: None, | ||||
|             boot_state: None, | ||||
|             memory: None, | ||||
|             uptime: None, | ||||
|             downtime: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Retrieve statistics for the go-sbot systemd process by querying `systemctl`. | ||||
| pub fn sbot_stats() -> Result<SbotStat, StatsError> { | ||||
|     let mut status = SbotStat::default(); | ||||
|  | ||||
|     let info_output = Command::new("sudo") | ||||
|         .arg("systemctl") | ||||
|         .arg("show") | ||||
|         .arg("go-sbot.service") | ||||
|         .arg("--no-page") | ||||
|         .output() | ||||
|         .map_err(StatsError::Systemctl)?; | ||||
|  | ||||
|     let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?; | ||||
|  | ||||
|     for line in service_info.lines() { | ||||
|         if line.starts_with("ActiveState=") { | ||||
|             if let Some(state) = line.strip_prefix("ActiveState=") { | ||||
|                 status.state = Some(state.to_string()) | ||||
|             } | ||||
|         } else if line.starts_with("MemoryCurrent=") { | ||||
|             if let Some(memory) = line.strip_prefix("MemoryCurrent=") { | ||||
|                 status.memory = memory.parse().ok() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let status_output = Command::new("sudo") | ||||
|         .arg("systemctl") | ||||
|         .arg("status") | ||||
|         .arg("go-sbot.service") | ||||
|         .output() | ||||
|         .map_err(StatsError::Systemctl)?; | ||||
|  | ||||
|     let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?; | ||||
|  | ||||
|     for line in service_status.lines() { | ||||
|         // example of the output line we're looking for: | ||||
|         // `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor | ||||
|         // preset: enabled)` | ||||
|         if line.contains("Loaded:") { | ||||
|             let before_boot_state = line.find(';'); | ||||
|             let after_boot_state = line.rfind(';'); | ||||
|             if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) { | ||||
|                 // extract the enabled / disabled from the `Loaded: ...` line | ||||
|                 // using the index of the first ';' + 2 and the last ';' | ||||
|                 status.boot_state = Some(line[start + 2..end].to_string()); | ||||
|             } | ||||
|         // example of the output line we're looking for here: | ||||
|         // `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago` | ||||
|         } else if line.contains("Active:") { | ||||
|             let before_time = line.find(';'); | ||||
|             let after_time = line.find(" ago"); | ||||
|             if let (Some(start), Some(end)) = (before_time, after_time) { | ||||
|                 // extract the uptime / downtime from the `Active: ...` line | ||||
|                 // using the index of ';' + 2 and the index of " ago" | ||||
|                 let time = Some(&line[start + 2..end]); | ||||
|                 // if service is active then the `time` reading is uptime | ||||
|                 if status.state == Some("active".to_string()) { | ||||
|                     status.uptime = time.map(|t| t.to_string()) | ||||
|                 // if service is inactive then the `time` reading is downtime | ||||
|                 } else if status.state == Some("inactive".to_string()) { | ||||
|                     status.downtime = time.map(|t| t.to_string()) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(status) | ||||
| } | ||||
							
								
								
									
										15
									
								
								peach-web-lite/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,15 @@ | ||||
| [package] | ||||
| name = "peach-web-lite" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| env_logger = "0.9.0" | ||||
| lazy_static = "1.4.0" | ||||
| log = "0.4.14" | ||||
| maud = "0.23.0" | ||||
| peach-lib = { path = "../peach-lib" } | ||||
| peach-network = { path = "../peach-network" } | ||||
| peach-stats = { path = "../peach-stats" } | ||||
| rouille = "3.5.0" | ||||
| golgi = { path = "/home/glyph/Projects/playground/rust/golgi" } | ||||
							
								
								
									
										24
									
								
								peach-web-lite/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | ||||
| # peach-web (lite) | ||||
|  | ||||
| A web interface for managing a Scuttlebutt pub. | ||||
|  | ||||
| ## Application Structure | ||||
|  | ||||
| The application is divided between the route handlers (`src/routes`), context-builders (`src/context`), templates (`src/templates`) and authentication helper functions. The web server itself is initialised in `src/main.rs`. The context-builders are responsible for retrieving data which is required by the templates. This allows separation of data retrieval and data representation (the job of the templates). | ||||
|  | ||||
| ## Built With | ||||
|  | ||||
| [Rouille](https://crates.io/crates/rouille): "a Rust web micro-framework". | ||||
| [maud](https://crates.io/crates/maud): "Compile-time HTML templates". | ||||
| [golgi](https://git.coopcloud.tech/golgi-ssb/golgi): "an experimental Scuttlebutt client library". | ||||
| [peach-lib](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-lib). | ||||
| [peach-network](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network). | ||||
| [peach-stats](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats) | ||||
|  | ||||
| ## Design Goals | ||||
|  | ||||
| Be lean and fast. | ||||
|  | ||||
| ## Licensing | ||||
|  | ||||
| AGPL-3.0 | ||||
							
								
								
									
										68
									
								
								peach-web-lite/ap_card
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,68 @@ | ||||
| {%- if ap_state == "up" %} | ||||
|     <!-- NETWORK CARD --> | ||||
|     <div class="card center"> | ||||
|       <!-- NETWORK INFO BOX --> | ||||
|       <div class="capsule capsule-container success-border"> | ||||
|         <!-- NETWORK STATUS GRID --> | ||||
|         <div class="two-grid" title="PeachCloud network mode and status"> | ||||
|           <!-- top-right config icon --> | ||||
|           <a class="link two-grid-top-right" href="/settings/network" title="Configure network settings"> | ||||
|             <img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure"> | ||||
|           </a> | ||||
|           <!-- left column --> | ||||
|           <!-- network mode icon with label --> | ||||
|           <div class="grid-column-1"> | ||||
|             <img id="netModeIcon" class="center icon icon-active" src="/icons/router.svg" alt="WiFi router"> | ||||
|             <label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online">ONLINE</label> | ||||
|           </div> | ||||
|           <!-- right column --> | ||||
|           <!-- network mode, ssid & ip with labels --> | ||||
|           <div class="grid-column-2"> | ||||
|             <label class="label-small font-gray" for="netMode" title="Network Mode">MODE</label> | ||||
|             <p id="netMode" class="card-text" title="Network Mode">Access Point</p> | ||||
|             <label class="label-small font-gray" for="netSsid" title="Access Point SSID">SSID</label> | ||||
|             <p id="netSsid" class="card-text" title="SSID">peach</p> | ||||
|             <label class="label-small font-gray" for="netIp" title="Access Point IP Address">IP</label> | ||||
|             <p id="netIp" class="card-text" title="IP">{{ ap_ip }}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|         <!-- horizontal dividing line --> | ||||
|         <hr> | ||||
|         <!-- DEVICES AND TRAFFIC GRID --> | ||||
|         <div class="three-grid card-container"> | ||||
|           <div class="stack"> | ||||
|             <img id="devices" class="icon icon-medium" title="Connected devices" src="/icons/devices.svg" alt="Digital devices"> | ||||
|             <div class="flex-grid" style="padding-top: 0.5rem;"> | ||||
|               <label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices"></label> | ||||
|             </div> | ||||
|             <label class="label-small font-gray">DEVICES</label> | ||||
|           </div> | ||||
|           <div class="stack"> | ||||
|             <img id="dataDownload" class="icon icon-medium" title="Download" src="/icons/down-arrow.svg" alt="Download"> | ||||
|             <div class="flex-grid" style="padding-top: 0.5rem;"> | ||||
|             {%- if ap_traffic -%} | ||||
|               <label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in {{ ap_traffic.rx_unit }}">{{ ap_traffic.received }}</label> | ||||
|               <label class="label-small font-near-black">{{ ap_traffic.rx_unit }}</label> | ||||
|             {%- else -%} | ||||
|               <label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total"></label> | ||||
|               <label class="label-small font-near-black"></label> | ||||
|             {%- endif -%} | ||||
|             </div> | ||||
|             <label class="label-small font-gray">DOWNLOAD</label> | ||||
|           </div> | ||||
|           <div class="stack"> | ||||
|             <img id="dataUpload" class="icon icon-medium" title="Upload" src="/icons/up-arrow.svg" alt="Upload"> | ||||
|             <div class="flex-grid" style="padding-top: 0.5rem;"> | ||||
|               {%- if ap_traffic -%} | ||||
|               <label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in {{ ap_traffic.tx_unit }}">{{ ap_traffic.transmitted }}</label> | ||||
|               <label class="label-small font-near-black">{{ ap_traffic.tx_unit }}</label> | ||||
|               {%- else -%} | ||||
|               <label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total"></label> | ||||
|               <label class="label-small font-near-black"></label> | ||||
|               {%- endif -%} | ||||
|             </div> | ||||
|             <label class="label-small font-gray">UPLOAD</label> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
							
								
								
									
										12
									
								
								peach-web-lite/compilation_comparions
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | ||||
|  | ||||
| [ peach-web-lite ] | ||||
|  | ||||
| 230 total dependencies | ||||
|  | ||||
| Finished release [optimized] target(s) in 35.12s | ||||
|  | ||||
| [ peach-web ] | ||||
|  | ||||
| 522 total dependencies | ||||
|  | ||||
| Finished release [optimized] target(s) in 1m 33s | ||||
							
								
								
									
										23
									
								
								peach-web-lite/src/auth.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | ||||
| use log::debug; | ||||
| use peach_lib::password_utils; | ||||
|  | ||||
| use crate::error::PeachWebError; | ||||
|  | ||||
| /// Password save form request handler. This function is for use by a user who is already logged in to change their password. | ||||
| pub fn save_password_form( | ||||
|     current_password: String, | ||||
|     new_password1: String, | ||||
|     new_password2: String, | ||||
| ) -> Result<(), PeachWebError> { | ||||
|     debug!("attempting to change password"); | ||||
|  | ||||
|     password_utils::verify_password(¤t_password)?; | ||||
|  | ||||
|     // if the previous line did not throw an error, then the old password is correct | ||||
|     password_utils::validate_new_passwords(&new_password1, &new_password2)?; | ||||
|  | ||||
|     // if the previous line did not throw an error, then the new password is valid | ||||
|     password_utils::set_new_password(&new_password1)?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										8
									
								
								peach-web-lite/src/context/admin.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,8 @@ | ||||
| use peach_lib::config_manager; | ||||
|  | ||||
| use crate::error::PeachWebError; | ||||
|  | ||||
| pub fn ssb_admin_ids() -> Result<Vec<String>, PeachWebError> { | ||||
|     let peach_config = config_manager::load_peach_config()?; | ||||
|     Ok(peach_config.ssb_admin_ids) | ||||
| } | ||||
							
								
								
									
										4
									
								
								peach-web-lite/src/context/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,4 @@ | ||||
| pub mod admin; | ||||
| pub mod network; | ||||
| pub mod status; | ||||
| pub mod test; | ||||
							
								
								
									
										402
									
								
								peach-web-lite/src/context/network.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,402 @@ | ||||
| //! Data retrieval for the purpose of hydrating HTML templates. | ||||
|  | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use log::info; | ||||
| use peach_lib::{ | ||||
|     config_manager, dyndns_client, | ||||
|     error::PeachError, | ||||
|     jsonrpc_client_core::{Error, ErrorKind}, | ||||
|     jsonrpc_core::types::error::ErrorCode, | ||||
| }; | ||||
| use peach_network::{ | ||||
|     network, | ||||
|     network::{Scan, Status, Traffic}, | ||||
| }; | ||||
|  | ||||
| use crate::error::PeachWebError; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct AccessPoint { | ||||
|     pub detail: Option<Scan>, | ||||
|     pub signal: Option<i32>, | ||||
|     pub state: String, | ||||
| } | ||||
|  | ||||
| pub fn ap_state() -> String { | ||||
|     match network::state("ap0") { | ||||
|         Ok(Some(state)) => state, | ||||
|         _ => "Interface unavailable".to_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)] | ||||
| pub struct ConfigureDNSContext { | ||||
|     pub external_domain: String, | ||||
|     pub dyndns_subdomain: String, | ||||
|     pub enable_dyndns: bool, | ||||
|     pub is_dyndns_online: bool, | ||||
| } | ||||
|  | ||||
| impl ConfigureDNSContext { | ||||
|     pub fn build() -> ConfigureDNSContext { | ||||
|         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, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // TODO: this should probably rather go into the appropriate `routes` file | ||||
| pub fn save_dns_configuration( | ||||
|     external_domain: String, | ||||
|     enable_dyndns: bool, | ||||
|     dynamic_domain: String, | ||||
| ) -> Result<(), PeachWebError> { | ||||
|     // first save local configurations | ||||
|     config_manager::set_external_domain(&external_domain)?; | ||||
|     config_manager::set_dyndns_enabled_value(enable_dyndns)?; | ||||
|  | ||||
|     // if dynamic dns is enabled and this is a new domain name, then register it | ||||
|     if enable_dyndns { | ||||
|         let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain); | ||||
|         // check if this is a new domain or if its already registered | ||||
|         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(_) => { | ||||
|                     info!("Registered new dyndns domain"); | ||||
|                     // successful update | ||||
|                     Ok(()) | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     info!("Failed to register dyndns domain: {:?}", err); | ||||
|                     // json response for failed update | ||||
|                     let msg: String = match err { | ||||
|                         // TODO: make this a nest high-level match | ||||
|                         // PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(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) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         _ => "Failed to register dyndns domain".to_string(), | ||||
|                     }; | ||||
|                     Err(PeachWebError::FailedToRegisterDynDomain(msg)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // if the domain is already registered, then dont re-register, and just return success | ||||
|         else { | ||||
|             Ok(()) | ||||
|         } | ||||
|     } else { | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct IfaceTraffic { | ||||
|     pub rx: u64, | ||||
|     pub rx_unit: String, | ||||
|     pub tx: u64, | ||||
|     pub tx_unit: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct NetworkDetailContext { | ||||
|     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 { | ||||
|         // TODO: read this value from the config file | ||||
|         let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|         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 { | ||||
|             saved_aps, | ||||
|             wlan_ip, | ||||
|             wlan_networks, | ||||
|             wlan_rssi, | ||||
|             wlan_ssid, | ||||
|             wlan_state, | ||||
|             wlan_status, | ||||
|             wlan_traffic, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct NetworkListContext { | ||||
|     pub ap_state: String, | ||||
|     pub wlan_networks: HashMap<String, String>, | ||||
|     pub wlan_ssid: String, | ||||
| } | ||||
|  | ||||
| impl NetworkListContext { | ||||
|     pub fn build() -> NetworkListContext { | ||||
|         // TODO: read these values from the config file | ||||
|         let ap_iface = "ap0".to_string(); | ||||
|         let wlan_iface = "wlan0".to_string(); | ||||
|         //let wlan_iface = "wlp0s20f0u2".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 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, | ||||
|             wlan_networks, | ||||
|             wlan_ssid, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| 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>, | ||||
| } | ||||
|  | ||||
| impl NetworkStatusContext { | ||||
|     pub fn build() -> Self { | ||||
|         // TODO: read these values from config file | ||||
|         let ap_iface = "ap0".to_string(); | ||||
|         let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|         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, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										76
									
								
								peach-web-lite/src/context/status.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,76 @@ | ||||
| use peach_stats::{stats, stats::LoadAverage}; | ||||
|  | ||||
| /// System statistics data. | ||||
| pub struct StatusContext { | ||||
|     pub cpu_usage_percent: Option<f32>, | ||||
|     pub disk_usage_percent: Option<u32>, | ||||
|     pub disk_free: Option<u64>, | ||||
|     pub load_average: Option<LoadAverage>, | ||||
|     pub mem_usage_percent: Option<u64>, | ||||
|     pub mem_used: Option<u64>, | ||||
|     pub mem_free: Option<u64>, | ||||
|     pub mem_total: Option<u64>, | ||||
|     pub uptime: Option<u32>, | ||||
| } | ||||
|  | ||||
| impl StatusContext { | ||||
|     pub fn build() -> StatusContext { | ||||
|         // convert result to Option<CpuStatPercentages>, discard any error | ||||
|         let cpu_usage_percent = stats::cpu_stats_percent() | ||||
|             .ok() | ||||
|             .map(|cpu| (cpu.nice + cpu.system + cpu.user).round()); | ||||
|  | ||||
|         let load_average = stats::load_average().ok(); | ||||
|  | ||||
|         let mem_stats = stats::mem_stats().ok(); | ||||
|         let (mem_usage_percent, mem_used, mem_free, mem_total) = match mem_stats { | ||||
|             Some(mem) => ( | ||||
|                 Some((mem.used / mem.total) * 100), | ||||
|                 Some(mem.used / 1024), | ||||
|                 Some(mem.free / 1024), | ||||
|                 Some(mem.total / 1024), | ||||
|             ), | ||||
|             None => (None, None, None, None), | ||||
|         }; | ||||
|  | ||||
|         let uptime = match stats::uptime() { | ||||
|             Ok(secs) => { | ||||
|                 let uptime_mins = secs / 60; | ||||
|                 uptime_mins.to_string() | ||||
|             } | ||||
|             Err(_) => "Unavailable".to_string(), | ||||
|         }; | ||||
|  | ||||
|         // parse the uptime string to a signed integer (for math) | ||||
|         let uptime_parsed = uptime.parse::<u32>().ok(); | ||||
|  | ||||
|         let disk_usage_stats = match stats::disk_usage() { | ||||
|             Ok(disks) => disks, | ||||
|             Err(_) => Vec::new(), | ||||
|         }; | ||||
|  | ||||
|         // select only the partition we're interested in: "/" | ||||
|         let disk_stats = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/"); | ||||
|  | ||||
|         let (disk_usage_percent, disk_free) = match disk_stats { | ||||
|             Some(disk) => ( | ||||
|                 Some(disk.used_percentage), | ||||
|                 // calculate free disk space in megabytes | ||||
|                 Some(disk.one_k_blocks_free / 1024), | ||||
|             ), | ||||
|             None => (None, None), | ||||
|         }; | ||||
|  | ||||
|         StatusContext { | ||||
|             cpu_usage_percent, | ||||
|             disk_usage_percent, | ||||
|             disk_free, | ||||
|             load_average, | ||||
|             mem_usage_percent, | ||||
|             mem_used, | ||||
|             mem_free, | ||||
|             mem_total, | ||||
|             uptime: uptime_parsed, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										12
									
								
								peach-web-lite/src/context/test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | ||||
| use golgi; | ||||
|  | ||||
| use golgi::sbot::Sbot; | ||||
|  | ||||
| pub async fn test_async() -> Result<(), Box<dyn std::error::Error>> { | ||||
|     let mut sbot_client = Sbot::init(Some("127.0.0.1:8009".to_string()), None).await?; | ||||
|  | ||||
|     let id = sbot_client.whoami().await?; | ||||
|     println!("whoami: {}", id); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										60
									
								
								peach-web-lite/src/error.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,60 @@ | ||||
| //! Custom error type representing all possible error variants for peach-web. | ||||
|  | ||||
| use peach_lib::error::PeachError; | ||||
| use peach_lib::{serde_json, serde_yaml}; | ||||
| use serde_json::error::Error as JsonError; | ||||
| use serde_yaml::Error as YamlError; | ||||
|  | ||||
| /// Custom error type encapsulating all possible errors for the web application. | ||||
| #[derive(Debug)] | ||||
| pub enum PeachWebError { | ||||
|     Json(JsonError), | ||||
|     Yaml(YamlError), | ||||
|     FailedToRegisterDynDomain(String), | ||||
|     PeachLib { source: PeachError, msg: String }, | ||||
| } | ||||
|  | ||||
| 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 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::PeachLib { | ||||
|             source: err, | ||||
|             msg: "".to_string(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								peach-web-lite/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,42 @@ | ||||
| use std::env; | ||||
|  | ||||
| use lazy_static::lazy_static; | ||||
| use log::info; | ||||
|  | ||||
| mod auth; | ||||
| mod context; | ||||
| mod error; | ||||
| mod router; | ||||
| mod routes; | ||||
| mod templates; | ||||
|  | ||||
| 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 | ||||
|     }; | ||||
| } | ||||
|  | ||||
| fn main() { | ||||
|     env_logger::init(); | ||||
|  | ||||
|     rouille::start_server("localhost:8000", move |request| { | ||||
|         info!("Now listening on localhost:8000"); | ||||
|  | ||||
|         // static file server: matches on assets in the `static` directory | ||||
|         if let Some(request) = request.remove_prefix("/static") { | ||||
|             return rouille::match_assets(&request, "static"); | ||||
|         } | ||||
|  | ||||
|         // configure the router based on run-mode | ||||
|         if *STANDALONE_MODE { | ||||
|             info!("Running in standalone mode"); | ||||
|             router::minimal_router(request) | ||||
|         } else { | ||||
|             info!("Running in fully-featured mode"); | ||||
|             router::complete_router(request) | ||||
|         } | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										194
									
								
								peach-web-lite/src/router.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,194 @@ | ||||
| use rouille::{router, Request, Response}; | ||||
|  | ||||
| use crate::routes; | ||||
|  | ||||
| /// Define router for standalone mode (PeachPub). | ||||
| pub fn minimal_router(request: &Request) -> Response { | ||||
|     router!(request, | ||||
|         (GET) (/) => { | ||||
|             routes::home::menu() | ||||
|         }, | ||||
|         (GET) (/help) => { | ||||
|             routes::help::menu() | ||||
|         }, | ||||
|         (GET) (/login) => { | ||||
|             routes::login::login() | ||||
|         }, | ||||
|         (POST) (/login) => { | ||||
|             routes::login::login_post(request) | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/blocks) => { | ||||
|             routes::scuttlebutt::blocks() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/follows) => { | ||||
|             routes::scuttlebutt::follows() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/followers) => { | ||||
|             routes::scuttlebutt::followers() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/friends) => { | ||||
|             routes::scuttlebutt::friends() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/peers) => { | ||||
|             routes::scuttlebutt::peers() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/private) => { | ||||
|             routes::scuttlebutt::private() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/profile) => { | ||||
|             routes::scuttlebutt::profile() | ||||
|         }, | ||||
|         (GET) (/settings) => { | ||||
|             routes::settings::menu() | ||||
|         }, | ||||
|         (GET) (/settings/admin) => { | ||||
|             routes::settings::admin() | ||||
|         }, | ||||
|         (GET) (/settings/admin/configure) => { | ||||
|             routes::settings::admin_configure() | ||||
|         }, | ||||
|         (GET) (/settings/admin/add) => { | ||||
|             routes::settings::admin_add() | ||||
|         }, | ||||
|         (POST) (/settings/admin/add) => { | ||||
|             routes::settings::admin_add_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/admin/change_password) => { | ||||
|             routes::settings::admin_change_password() | ||||
|         }, | ||||
|         (POST) (/settings/admin/change_password) => { | ||||
|             routes::settings::admin_change_password_post(request) | ||||
|         }, | ||||
|         (POST) (/settings/admin/delete) => { | ||||
|             routes::settings::admin_delete_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/scuttlebutt) => { | ||||
|             routes::settings::scuttlebutt() | ||||
|         }, | ||||
|         (GET) (/status) => { | ||||
|             routes::status::status() | ||||
|         }, | ||||
|         // return 404 if not match is found | ||||
|         _ => routes::catchers::not_found() | ||||
|     ) | ||||
| } | ||||
|  | ||||
| /// Define router for fully-featured mode (PeachCloud). | ||||
| pub fn complete_router(request: &Request) -> Response { | ||||
|     router!(request, | ||||
|         (GET) (/async) => { | ||||
|             routes::home::async_test() | ||||
|         }, | ||||
|         (GET) (/) => { | ||||
|             routes::home::menu() | ||||
|         }, | ||||
|         (GET) (/help) => { | ||||
|             routes::help::menu() | ||||
|         }, | ||||
|         (GET) (/login) => { | ||||
|             routes::login::login() | ||||
|         }, | ||||
|         (POST) (/login) => { | ||||
|             routes::login::login_post(request) | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/blocks) => { | ||||
|             routes::scuttlebutt::blocks() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/follows) => { | ||||
|             routes::scuttlebutt::follows() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/followers) => { | ||||
|             routes::scuttlebutt::followers() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/friends) => { | ||||
|             routes::scuttlebutt::friends() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/peers) => { | ||||
|             routes::scuttlebutt::peers() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/private) => { | ||||
|             routes::scuttlebutt::private() | ||||
|         }, | ||||
|         (GET) (/scuttlebutt/profile) => { | ||||
|             routes::scuttlebutt::profile() | ||||
|         }, | ||||
|         (GET) (/settings) => { | ||||
|             routes::settings::menu() | ||||
|         }, | ||||
|         (GET) (/settings/admin) => { | ||||
|             routes::settings::admin() | ||||
|         }, | ||||
|         (GET) (/settings/admin/configure) => { | ||||
|             routes::settings::admin_configure() | ||||
|         }, | ||||
|         (GET) (/settings/admin/add) => { | ||||
|             routes::settings::admin_add() | ||||
|         }, | ||||
|         (POST) (/settings/admin/add) => { | ||||
|             routes::settings::admin_add_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/admin/change_password) => { | ||||
|             routes::settings::admin_change_password() | ||||
|         }, | ||||
|         (POST) (/settings/admin/change_password) => { | ||||
|             routes::settings::admin_change_password_post(request) | ||||
|         }, | ||||
|         (POST) (/settings/admin/delete) => { | ||||
|             routes::settings::admin_delete_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/network) => { | ||||
|             routes::settings::network() | ||||
|         }, | ||||
|         (GET) (/settings/network/wifi) => { | ||||
|             routes::settings::network_list_aps() | ||||
|         }, | ||||
|         (POST) (/settings/network/wifi/connect) => { | ||||
|             routes::settings::network_connect_wifi(request) | ||||
|         }, | ||||
|         (POST) (/settings/network/wifi/disconnect) => { | ||||
|             routes::settings::network_disconnect_wifi(request) | ||||
|         }, | ||||
|         (POST) (/settings/network/wifi/forget) => { | ||||
|             routes::settings::network_forget_wifi(request) | ||||
|         }, | ||||
|         (GET) (/settings/network/wifi/ssid/{ssid: String}) => { | ||||
|             routes::settings::network_detail(ssid) | ||||
|         }, | ||||
|         (GET) (/settings/network/wifi/add) => { | ||||
|             routes::settings::network_add_ap(None) | ||||
|         }, | ||||
|         (GET) (/settings/network/wifi/add/{ssid: String}) => { | ||||
|             routes::settings::network_add_ap(Some(ssid)) | ||||
|         }, | ||||
|         (POST) (/settings/network/wifi/add) => { | ||||
|             routes::settings::network_add_ap_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/network/wifi/modify) => { | ||||
|             routes::settings::network_modify_ap(None) | ||||
|         }, | ||||
|         // TODO: see if we can use the ?= syntax for ssid param | ||||
|         (GET) (/settings/network/wifi/modify/{ssid: String}) => { | ||||
|             routes::settings::network_modify_ap(Some(ssid)) | ||||
|         }, | ||||
|         (POST) (/settings/network/wifi/modify) => { | ||||
|             routes::settings::network_modify_ap_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/network/dns) => { | ||||
|             routes::settings::network_configure_dns() | ||||
|         }, | ||||
|         (POST) (/settings/network/dns) => { | ||||
|             routes::settings::network_configure_dns_post(request) | ||||
|         }, | ||||
|         (GET) (/settings/scuttlebutt) => { | ||||
|             routes::settings::scuttlebutt() | ||||
|         }, | ||||
|         (GET) (/status) => { | ||||
|             routes::status::status() | ||||
|         }, | ||||
|         (GET) (/status/network) => { | ||||
|             routes::status::network() | ||||
|         }, | ||||
|         // return 404 if not match is found | ||||
|         _ => routes::catchers::not_found() | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										10
									
								
								peach-web-lite/src/routes/catchers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,10 @@ | ||||
| use log::debug; | ||||
| use rouille::Response; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn not_found() -> Response { | ||||
|     debug!("received GET request for a route which is not defined"); | ||||
|  | ||||
|     Response::html(templates::catchers::not_found()).with_status_code(404) | ||||
| } | ||||
							
								
								
									
										10
									
								
								peach-web-lite/src/routes/help.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,10 @@ | ||||
| use log::debug; | ||||
| use rouille::Response; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn menu() -> Response { | ||||
|     debug!("received GET request for: /help"); | ||||
|  | ||||
|     Response::html(templates::help::menu()) | ||||
| } | ||||
							
								
								
									
										14
									
								
								peach-web-lite/src/routes/home.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,14 @@ | ||||
| use log::debug; | ||||
| use rouille::Response; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn menu() -> Response { | ||||
|     debug!("received GET request for: /"); | ||||
|  | ||||
|     Response::html(templates::home::menu()) | ||||
| } | ||||
|  | ||||
| pub fn async_test() -> Response { | ||||
|     Response::html(templates::home::async_test()) | ||||
| } | ||||
							
								
								
									
										26
									
								
								peach-web-lite/src/routes/login.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | ||||
| use log::debug; | ||||
| use rouille::{post_input, try_or_400, Request, Response}; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn login() -> Response { | ||||
|     debug!("received GET request for: /login"); | ||||
|  | ||||
|     Response::html(templates::login::login()) | ||||
| } | ||||
|  | ||||
| pub fn login_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /login"); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { | ||||
|         username: String, | ||||
|         password: String, | ||||
|     })); | ||||
|  | ||||
|     // TODO: handle authentication... | ||||
|  | ||||
|     debug!("{:?}", data); | ||||
|  | ||||
|     // TODO: add flash message | ||||
|     Response::redirect_302("/") | ||||
| } | ||||
							
								
								
									
										7
									
								
								peach-web-lite/src/routes/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,7 @@ | ||||
| pub mod catchers; | ||||
| pub mod help; | ||||
| pub mod home; | ||||
| pub mod login; | ||||
| pub mod scuttlebutt; | ||||
| pub mod settings; | ||||
| pub mod status; | ||||
							
								
								
									
										46
									
								
								peach-web-lite/src/routes/scuttlebutt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,46 @@ | ||||
| use log::debug; | ||||
| use rouille::Response; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn blocks() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/blocks"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::peers_list("Blocks".to_string())) | ||||
| } | ||||
|  | ||||
| pub fn follows() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/follows"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::peers_list("Follows".to_string())) | ||||
| } | ||||
|  | ||||
| pub fn followers() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/followers"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::peers_list("Followers".to_string())) | ||||
| } | ||||
|  | ||||
| pub fn friends() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/friends"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::peers_list("Friends".to_string())) | ||||
| } | ||||
|  | ||||
| pub fn peers() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/peers"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::peers()) | ||||
| } | ||||
|  | ||||
| pub fn private() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/private"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::private()) | ||||
| } | ||||
|  | ||||
| pub fn profile() -> Response { | ||||
|     debug!("received GET request for: /scuttlebutt/profile"); | ||||
|  | ||||
|     Response::html(templates::scuttlebutt::profile(None, None)) | ||||
| } | ||||
							
								
								
									
										340
									
								
								peach-web-lite/src/routes/settings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,340 @@ | ||||
| use log::{debug, warn}; | ||||
| use peach_lib::config_manager; | ||||
| use peach_network::network; | ||||
| use rouille::{post_input, try_or_400, Request, Response}; | ||||
|  | ||||
| use crate::auth; | ||||
| use crate::context; | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn menu() -> Response { | ||||
|     debug!("received GET request for: /settings"); | ||||
|  | ||||
|     Response::html(templates::settings::menu::menu()) | ||||
| } | ||||
|  | ||||
| pub fn admin() -> Response { | ||||
|     debug!("received GET request for: /settings/admin"); | ||||
|  | ||||
|     Response::html(templates::settings::admin::menu()) | ||||
| } | ||||
|  | ||||
| pub fn admin_configure() -> Response { | ||||
|     debug!("received GET request for: /settings/admin/configure"); | ||||
|  | ||||
|     Response::html(templates::settings::admin::configure(None, None)) | ||||
| } | ||||
|  | ||||
| pub fn admin_add() -> Response { | ||||
|     debug!("received GET request for: /settings/admin/add"); | ||||
|  | ||||
|     Response::html(templates::settings::admin::add(None, None)) | ||||
| } | ||||
|  | ||||
| pub fn admin_add_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/admin/add"); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssb_id: String })); | ||||
|  | ||||
|     debug!("{:?}", data); | ||||
|  | ||||
|     let (flash_name, flash_msg) = match config_manager::add_ssb_admin_id(&data.ssb_id) { | ||||
|         Ok(_) => ( | ||||
|             "success".to_string(), | ||||
|             "Added new SSB administrator ID".to_string(), | ||||
|         ), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to add new SSB administrator ID: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::admin::add( | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn admin_change_password() -> Response { | ||||
|     debug!("received GET request for: /settings/admin/change_password"); | ||||
|  | ||||
|     Response::html(templates::settings::admin::change_password(None, None)) | ||||
| } | ||||
|  | ||||
| pub fn admin_change_password_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/admin/change_password"); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { | ||||
|         current_password: String, | ||||
|         new_password1: String, | ||||
|         new_password2: String | ||||
|     })); | ||||
|  | ||||
|     // attempt to update the password | ||||
|     let (flash_name, flash_msg) = match auth::save_password_form( | ||||
|         data.current_password, | ||||
|         data.new_password1, | ||||
|         data.new_password2, | ||||
|     ) { | ||||
|         Ok(_) => ("success".to_string(), "Saved new password".to_string()), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to save new password: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::admin::change_password( | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn admin_delete_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/admin/delete"); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssb_id: String })); | ||||
|  | ||||
|     let (flash_name, flash_msg) = match config_manager::delete_ssb_admin_id(&data.ssb_id) { | ||||
|         Ok(_) => ( | ||||
|             "success".to_string(), | ||||
|             "Removed SSB administrator ID".to_string(), | ||||
|         ), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to remove SSB administrator ID: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::admin::configure( | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network() -> Response { | ||||
|     debug!("received GET request for: /settings/network"); | ||||
|  | ||||
|     Response::html(templates::settings::network::menu()) | ||||
| } | ||||
|  | ||||
| pub fn network_add_ap(ssid: Option<String>) -> Response { | ||||
|     debug!("received GET request for: /settings/network/wifi/add"); | ||||
|  | ||||
|     Response::html(templates::settings::network::add_ap(ssid, None, None)) | ||||
| } | ||||
|  | ||||
| pub fn network_add_ap_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/network/wifi/add"); | ||||
|  | ||||
|     // TODO: read this value from the config file instead | ||||
|     let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssid: String, pass: String })); | ||||
|  | ||||
|     /* ADD WIFI CREDENTIALS FOR AP */ | ||||
|  | ||||
|     // check if the credentials already exist for this access point | ||||
|     let creds_exist = match network::saved_networks() { | ||||
|         Ok(Some(ssids)) => ssids.contains(&data.ssid), | ||||
|         _ => false, | ||||
|     }; | ||||
|  | ||||
|     let (flash_name, flash_msg) = if creds_exist { | ||||
|         ( | ||||
|             "error".to_string(), | ||||
|             "Network credentials already exist for this access point".to_string(), | ||||
|         ) | ||||
|     } else { | ||||
|         // if credentials not found, generate and write wifi config to wpa_supplicant | ||||
|         match network::add(&wlan_iface, &data.ssid, &data.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)) | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::network::add_ap( | ||||
|         None, | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_configure_dns() -> Response { | ||||
|     debug!("received GET request for: /settings/network/dns"); | ||||
|  | ||||
|     Response::html(templates::settings::network::configure_dns(None, None)) | ||||
| } | ||||
|  | ||||
| pub fn network_configure_dns_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/network/dns"); | ||||
|  | ||||
|     let data = try_or_400!( | ||||
|         post_input!(request, { external_domain: String, enable_dyndns: bool, dynamic_domain: String }) | ||||
|     ); | ||||
|  | ||||
|     let (flash_name, flash_msg) = match context::network::save_dns_configuration( | ||||
|         data.external_domain, | ||||
|         data.enable_dyndns, | ||||
|         data.dynamic_domain, | ||||
|     ) { | ||||
|         Ok(_) => ( | ||||
|             "success".to_string(), | ||||
|             "New dynamic DNS configuration is now enabled".to_string(), | ||||
|         ), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to save DNS configuration: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::network::configure_dns( | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_modify_ap(ssid: Option<String>) -> Response { | ||||
|     debug!("received GET request for: /settings/network/wifi/modify"); | ||||
|  | ||||
|     Response::html(templates::settings::network::modify_ap(ssid, None, None)) | ||||
| } | ||||
|  | ||||
| pub fn network_modify_ap_post(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/network/wifi/modify"); | ||||
|  | ||||
|     // TODO: read this value from the config file instead | ||||
|     let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssid: String, pass: String })); | ||||
|  | ||||
|     /* MODIFY WIFI CREDENTIALS FOR AP */ | ||||
|     let (flash_name, flash_msg) = match network::update(&wlan_iface, &data.ssid, &data.pass) { | ||||
|         Ok(_) => ("success".to_string(), "WiFi password updated".to_string()), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to update WiFi password: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::network::modify_ap( | ||||
|         None, | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_connect_wifi(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/network/wifi/connect"); | ||||
|  | ||||
|     // TODO: read this value from the config file instead | ||||
|     let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssid: String })); | ||||
|  | ||||
|     let (flash_name, flash_msg) = match network::id(&wlan_iface, &data.ssid) { | ||||
|         Ok(Some(id)) => match network::connect(&id, &wlan_iface) { | ||||
|             Ok(_) => ( | ||||
|                 "success".to_string(), | ||||
|                 "Connected to chosen network".to_string(), | ||||
|             ), | ||||
|             Err(e) => ( | ||||
|                 "error".to_string(), | ||||
|                 format!("Failed to connect to chosen network: {}", e), | ||||
|             ), | ||||
|         }, | ||||
|  | ||||
|         _ => ( | ||||
|             "error".to_string(), | ||||
|             "Failed to retrieve the network ID".to_string(), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::network::network_detail( | ||||
|         data.ssid, | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_disconnect_wifi(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/network/wifi/disconnect"); | ||||
|  | ||||
|     // TODO: read this value from the config file instead | ||||
|     let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssid: String })); | ||||
|  | ||||
|     let (flash_name, flash_msg) = match network::disable(&wlan_iface, &data.ssid) { | ||||
|         Ok(_) => ( | ||||
|             "success".to_string(), | ||||
|             "Disconnected from WiFi network".to_string(), | ||||
|         ), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to disconnect from WiFi network: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::network::network_detail( | ||||
|         data.ssid, | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_forget_wifi(request: &Request) -> Response { | ||||
|     debug!("received POST request for: /settings/network/wifi/forget"); | ||||
|  | ||||
|     // TODO: read this value from the config file instead | ||||
|     let wlan_iface = "wlan0".to_string(); | ||||
|  | ||||
|     let data = try_or_400!(post_input!(request, { ssid: String })); | ||||
|  | ||||
|     let (flash_name, flash_msg) = match network::forget(&wlan_iface, &data.ssid) { | ||||
|         Ok(_) => ( | ||||
|             "success".to_string(), | ||||
|             "WiFi credentials removed".to_string(), | ||||
|         ), | ||||
|         Err(e) => ( | ||||
|             "error".to_string(), | ||||
|             format!("Failed to remove WiFi credentials: {}", e), | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     Response::html(templates::settings::network::network_detail( | ||||
|         data.ssid, | ||||
|         Some(flash_msg), | ||||
|         Some(flash_name), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_detail(ssid: String) -> Response { | ||||
|     debug!("received GET request for: /settings/network/wifi/<selected>"); | ||||
|  | ||||
|     Response::html(templates::settings::network::network_detail( | ||||
|         ssid, None, None, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub fn network_list_aps() -> Response { | ||||
|     debug!("received GET request for: /settings/network/wifi"); | ||||
|  | ||||
|     Response::html(templates::settings::network::list_aps()) | ||||
| } | ||||
|  | ||||
| pub fn scuttlebutt() -> Response { | ||||
|     debug!("received GET request for: /settings/scuttlebutt"); | ||||
|  | ||||
|     Response::html(templates::settings::scuttlebutt::scuttlebutt()) | ||||
| } | ||||
							
								
								
									
										16
									
								
								peach-web-lite/src/routes/status.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,16 @@ | ||||
| use log::debug; | ||||
| use rouille::Response; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn network() -> Response { | ||||
|     debug!("received GET request for: /status/network"); | ||||
|  | ||||
|     Response::html(templates::status::network()) | ||||
| } | ||||
|  | ||||
| pub fn status() -> Response { | ||||
|     debug!("received GET request for: /status"); | ||||
|  | ||||
|     Response::html(templates::status::status()) | ||||
| } | ||||
							
								
								
									
										51
									
								
								peach-web-lite/src/templates/base.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,51 @@ | ||||
| use maud::{html, PreEscaped, DOCTYPE}; | ||||
|  | ||||
| pub fn base(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> { | ||||
|     html! { | ||||
|         (DOCTYPE) | ||||
|         html lang="en" { | ||||
|             head { | ||||
|                 meta charset="utf-8"; | ||||
|                 title { "PeachCloud" } | ||||
|                 meta name="description" content="PeachCloud Network"; | ||||
|                 meta name="author" content="glyph"; | ||||
|                 meta name="viewport" content="width=device-width, initial-scale=1.0"; | ||||
|                 link rel="icon" type="image/x-icon" href="/static/icons/peach-icon.png"; | ||||
|                 link rel="stylesheet" href="/static/css/peachcloud.css"; | ||||
|             } | ||||
|             body { | ||||
|                 // render the navigation template | ||||
|                 (nav(back, title, content)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn nav(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> { | ||||
|     html! { | ||||
|         (PreEscaped("<!-- TOP NAV BAR -->")) | ||||
|         nav class="nav-bar" { | ||||
|             a class="nav-item" href=(back) title="Back" { | ||||
|                 img class="icon-medium nav-icon-left icon-active" src="/static/icons/back.svg" alt="Back"; | ||||
|             } | ||||
|             h1 class="nav-title" { (title) } | ||||
|             a class="nav-item" id="logoutButton" href="/logout" title="Logout" { | ||||
|                 img class="icon-medium nav-icon-right icon-active" src="/static/icons/enter.svg" alt="Enter"; | ||||
|             } | ||||
|         } | ||||
|         (PreEscaped("<!-- MAIN CONTENT CONTAINER -->")) | ||||
|         main { (content) } | ||||
|         (PreEscaped("<!-- BOTTOM NAV BAR -->")) | ||||
|         nav class="nav-bar" { | ||||
|             a class="nav-item" href="https://scuttlebutt.nz/" { | ||||
|                 img class="icon-medium nav-icon-left" title="Scuttlebutt Website" src="/static/icons/hermies.png" alt="Secure Scuttlebutt"; | ||||
|             } | ||||
|             a class="nav-item" href="/" { | ||||
|                 img class="icon nav-icon-left" src="/static/icons/peach-icon.png" alt="PeachCloud" title="Home"; | ||||
|             } | ||||
|             a class="nav-item" href="/power" { | ||||
|                 img class="icon-medium nav-icon-right icon-active" title="Shutdown" src="/static/icons/power.svg" alt="Power switch"; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								peach-web-lite/src/templates/catchers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,21 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| pub fn not_found() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "404 Not Found".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         div class="card center" { | ||||
|             div class="capsule-container" { | ||||
|                 div class="capsule info-border" { | ||||
|                     p { "No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct." } | ||||
|                     p { "Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home." } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										19
									
								
								peach-web-lite/src/templates/help.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,19 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| // /help | ||||
| pub fn menu() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "Help".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 p { "help content goes here" } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										77
									
								
								peach-web-lite/src/templates/home.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,77 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::context; | ||||
| use crate::templates; | ||||
|  | ||||
| pub async fn async_test() -> PreEscaped<String> { | ||||
|     let back = "".to_string(); | ||||
|     let title = "".to_string(); | ||||
|  | ||||
|     let whoami = context::test::test_async().await.unwrap(); | ||||
|  | ||||
|     let content = html! { | ||||
|         p { (whoami) } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| pub fn menu() -> PreEscaped<String> { | ||||
|     let back = "".to_string(); | ||||
|     let title = "".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- RADIAL MENU -->")) | ||||
|         div class="grid" { | ||||
|             (PreEscaped("<!-- top-left -->")) | ||||
|             (PreEscaped("<!-- PEERS LINK AND ICON -->")) | ||||
|             a class="top-left" href="/scuttlebutt/peers" title="Scuttlebutt Peers" { | ||||
|                 div class="circle circle-small" { | ||||
|                     img class="icon-medium" src="/static/icons/users.svg"; | ||||
|                 } | ||||
|             } | ||||
|             (PreEscaped("<!-- top-middle -->")) | ||||
|             (PreEscaped("<!-- CURRENT USER LINK AND ICON -->")) | ||||
|             a class="top-middle" href="/scuttlebutt/profile" title="Profile" { | ||||
|                 div class="circle circle-small" { | ||||
|                     img class="icon-medium" src="/static/icons/user.svg"; | ||||
|                 } | ||||
|             } | ||||
|             (PreEscaped("<!-- top-right -->")) | ||||
|             (PreEscaped("<!-- MESSAGES LINK AND ICON -->")) | ||||
|             a class="top-right" href="/scuttlebutt/private" title="Private Messages" { | ||||
|                 div class="circle circle-small" { | ||||
|                     img class="icon-medium" src="/static/icons/envelope.svg"; | ||||
|                 } | ||||
|             } | ||||
|             (PreEscaped("<!-- middle -->")) | ||||
|             a class="middle" href="/hello" { | ||||
|                 div class="circle circle-large" { } | ||||
|             } | ||||
|             (PreEscaped("<!-- bottom-left -->")) | ||||
|             (PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->")) | ||||
|             a class="bottom-left" href="/status" title="Status" { | ||||
|                 div class="circle circle-small" { | ||||
|                     img class="icon-medium" src="/static/icons/heart-pulse.svg"; | ||||
|                 } | ||||
|             } | ||||
|             (PreEscaped("<!-- bottom-middle -->")) | ||||
|             (PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->")) | ||||
|             a class="bottom-middle" href="/help" title="Help Menu" { | ||||
|                 div class="circle circle-small" { | ||||
|                     img class="icon-medium" src="/static/icons/book.svg"; | ||||
|                 } | ||||
|             } | ||||
|             (PreEscaped("<!-- bottom-right -->")) | ||||
|             (PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->")) | ||||
|             a class="bottom-right" href="/settings" title="Settings Menu" { | ||||
|                 div class="circle circle-small" { | ||||
|                     img class="icon-medium" src="/static/icons/cog.svg"; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // we pass the content of this template into the base template | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										30
									
								
								peach-web-lite/src/templates/login.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,30 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| // https://github.com/tomaka/rouille/blob/master/examples/login-session.rs | ||||
|  | ||||
| // /login | ||||
| pub fn login() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "Login".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 form id="login_form" action="/login" method="post" { | ||||
|                     // input field for username | ||||
|                     input id="username" name="username" class="center input" type="text" placeholder="Username" title="Username for authentication" autofocus { } | ||||
|                     // input field for password | ||||
|                     input id="password" name="password" class="center input" type="password" placeholder="Password" title="Password for given username" { } | ||||
|                     div id="buttonDiv" { | ||||
|                         // login button | ||||
|                         input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login" { } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										9
									
								
								peach-web-lite/src/templates/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,9 @@ | ||||
| pub mod base; | ||||
| pub mod catchers; | ||||
| pub mod help; | ||||
| pub mod home; | ||||
| pub mod login; | ||||
| pub mod scuttlebutt; | ||||
| pub mod settings; | ||||
| pub mod snippets; | ||||
| pub mod status; | ||||
							
								
								
									
										109
									
								
								peach-web-lite/src/templates/scuttlebutt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,109 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| // /scuttlebutt/peers | ||||
| pub fn peers() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "Scuttlebutt Peers".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- SCUTTLEBUTT PEERS -->")) | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 (PreEscaped("<!-- BUTTONS -->")) | ||||
|                 div id="buttons" { | ||||
|                     a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List Friends" { "Friends" } | ||||
|                     a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List Follows" { "Follows" } | ||||
|                     a id="followers" class="button button-primary center" href="/scuttlebutt/followers" title="List Followers" { "Followers" } | ||||
|                     a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List Blocks" { "Blocks" } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /scuttlebutt/friends | ||||
| // /scuttlebutt/follows | ||||
| // /scuttlebutt/followers | ||||
| // /scuttlebutt/blocks | ||||
| pub fn peers_list(title: String) -> PreEscaped<String> { | ||||
|     let back = "/scuttlebutt/peers".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- SCUTTLEBUTT PEERS LIST -->")) | ||||
|         div class="card center" { | ||||
|             ul class="list" { | ||||
|                 // for peer in peers | ||||
|                 li { | ||||
|                     a class="list-item link light-bg" href="/scuttlebutt/profile/(pub_key)" { | ||||
|                         img id="peerImage" class="icon list-icon" src="{ image_path }" alt="{ peer_name }'s profile image"; | ||||
|                         p id="peerName" class="list-text" { "(name)" } | ||||
|                         label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{ peer_name }'s Public Key" { "(public_key)" } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /scuttlebutt/private | ||||
| pub fn private() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "Private Messages".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 p { "private message content goes here" } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /scuttlebutt/profile | ||||
| pub fn profile(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "Scuttlebutt Profile".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- USER PROFILE -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- PROFILE INFO BOX -->")) | ||||
|             div class="capsule capsule-profile" title="Scuttlebutt account profile information" { | ||||
|                 (PreEscaped("<!-- edit profile button -->")) | ||||
|                 img id="editProfile" class="icon-small nav-icon-right" src="/icons/pencil.svg" alt="Profile picture"; | ||||
|                 (PreEscaped("<!-- PROFILE BIO -->")) | ||||
|                 (PreEscaped("<!-- profile picture -->")) | ||||
|                 img id="profilePicture" class="icon-large" src="{ image_path }" alt="Profile picture"; | ||||
|                 (PreEscaped("<!-- name, public key & description -->")) | ||||
|                 p id="profileName" class="card-text" title="Name" { "(name)" } | ||||
|                 label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" { "(public_key)" } | ||||
|                 p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" { "(description)" } | ||||
|             } | ||||
|             (PreEscaped("<!-- PUBLIC POST FORM -->")) | ||||
|             form id="postForm" action="/scuttlebutt/post" method="post" { | ||||
|                 (PreEscaped("<!-- input for message contents -->")) | ||||
|                 textarea id="publicPost" class="center input message-input" title="Compose Public Post" { } | ||||
|                 input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish"; | ||||
|             } | ||||
|             (PreEscaped("<!-- BUTTONS -->")) | ||||
|             (PreEscaped("<!-- TODO: each of these buttons needs to be a form with a public key -->")) | ||||
|             div id="buttons" { | ||||
|                 a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer" { "Follow" } | ||||
|                 a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer" { "Block" } | ||||
|                 a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private_message" title="Private Message" { "Private Message" } | ||||
|             } | ||||
|             (PreEscaped("<!-- FLASH MESSAGE -->")) | ||||
|             (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										122
									
								
								peach-web-lite/src/templates/settings/admin.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,122 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::context::admin; | ||||
| use crate::templates; | ||||
|  | ||||
| // /settings/admin | ||||
| pub fn menu() -> PreEscaped<String> { | ||||
|     let back = "/settings".to_string(); | ||||
|     let title = "Administrator Settings".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- ADMIN SETTINGS MENU -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- BUTTONS -->")) | ||||
|             div id="settingsButtons" { | ||||
|                 a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password" { "Change Password" } | ||||
|                 a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin" { "Configure Admin" } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/admin/add | ||||
| pub fn add(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> { | ||||
|     let back = "/settings/admin/configure".to_string(); | ||||
|     let title = "Add Administrator".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- ADD ADMIN FORM -->")) | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 form id="addAdminForm" action="/settings/admin/add" method="post" { | ||||
|                     input id="ssb_id" name="ssb_id" class="center input" type="text" placeholder="SSB ID" title="SSB ID of Admin" value=""; | ||||
|                     div id="buttonDiv" { | ||||
|                         input id="addAdmin" class="button button-primary center" title="Add" type="submit" value="Add"; | ||||
|                         a class="button button-secondary center" href="/settings/admin/configure" title="Cancel" { "Cancel" } | ||||
|                     } | ||||
|                 } | ||||
|                 // render flash message, if any | ||||
|                 (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/admin/configure | ||||
| pub fn configure(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> { | ||||
|     let ssb_admin_ids = admin::ssb_admin_ids(); | ||||
|  | ||||
|     let back = "/settings/admin".to_string(); | ||||
|     let title = "Administrator Configuration".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- CONFIGURE ADMIN PAGE -->")) | ||||
|         div class="card center" { | ||||
|             div class="text-container" { | ||||
|                 h4 { "Current Admins" } | ||||
|                 @match ssb_admin_ids { | ||||
|                     Ok(admins) => { | ||||
|                         @if admins.is_empty() { | ||||
|                             div { "No administators are currently configured" } | ||||
|                         } else { | ||||
|                             @for admin in admins { | ||||
|                                 div { | ||||
|                                     form action="/settings/admin/delete" method="post" { | ||||
|                                         input type="hidden" name="ssb_id" value="{{admin}}"; | ||||
|                                         input type="submit" value="X" title="Delete" { | ||||
|                                             span { (admin) } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     Err(e) => div { "Encountered an error while trying to retrieve list of administrators: " (e) } | ||||
|                 } | ||||
|                 a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin" { "Add Admin" } | ||||
|             } | ||||
|             // render flash message, if any | ||||
|             (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/admin/change_password | ||||
| pub fn change_password( | ||||
|     flash_msg: Option<String>, | ||||
|     flash_name: Option<String>, | ||||
| ) -> PreEscaped<String> { | ||||
|     let back = "/settings/admin".to_string(); | ||||
|     let title = "Change Password".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- CHANGE PASSWORD FORM -->")) | ||||
|         div class="card center" { | ||||
|             div class="form-container" { | ||||
|                 form id="changePassword" action="/settings/admin/change_password" method="post" { | ||||
|                     (PreEscaped("<!-- input for current password -->")) | ||||
|                     input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus; | ||||
|                     (PreEscaped("<!-- input for new password -->")) | ||||
|                     input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password"; | ||||
|                     (PreEscaped("<!-- input for duplicate new password -->")) | ||||
|                     input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" placeholder="Re-enter new password" title="New password duplicate"; | ||||
|                     div id="buttonDiv" { | ||||
|                         input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save"; | ||||
|                         a class="button button-secondary center" href="/settings/admin" title="Cancel" { "Cancel" } | ||||
|                     } | ||||
|                 } | ||||
|                 // render flash message, if any | ||||
|                 (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										37
									
								
								peach-web-lite/src/templates/settings/menu.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,37 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::{templates, STANDALONE_MODE}; | ||||
|  | ||||
| // /settings | ||||
| pub fn menu() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = "Settings".to_string(); | ||||
|  | ||||
|     // render a minimal menu (no network settings) if running in standalone mode | ||||
|     let content = if *STANDALONE_MODE { | ||||
|         html! { | ||||
|             (PreEscaped("<!-- SETTINGS MENU -->")) | ||||
|             div class="card center" { | ||||
|                 (PreEscaped("<!-- BUTTONS -->")) | ||||
|                 div id="settingsButtons" { | ||||
|                     a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" } | ||||
|                     a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         html! { | ||||
|             (PreEscaped("<!-- SETTINGS MENU -->")) | ||||
|             div class="card center" { | ||||
|                 (PreEscaped("<!-- BUTTONS -->")) | ||||
|                 div id="settingsButtons" { | ||||
|                     a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" } | ||||
|                     a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" } | ||||
|                     a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										4
									
								
								peach-web-lite/src/templates/settings/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,4 @@ | ||||
| pub mod admin; | ||||
| pub mod menu; | ||||
| pub mod network; | ||||
| pub mod scuttlebutt; | ||||
							
								
								
									
										319
									
								
								peach-web-lite/src/templates/settings/network.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,319 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::context::{ | ||||
|     network, | ||||
|     network::{ConfigureDNSContext, NetworkDetailContext, NetworkListContext}, | ||||
| }; | ||||
| use crate::templates; | ||||
|  | ||||
| // /settings/network/wifi/add | ||||
| pub fn add_ap( | ||||
|     ssid: Option<String>, | ||||
|     flash_msg: Option<String>, | ||||
|     flash_name: Option<String>, | ||||
| ) -> PreEscaped<String> { | ||||
|     let back = "/settings/network".to_string(); | ||||
|     let title = "Add WiFi Network".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- NETWORK ADD CREDENTIALS FORM -->")) | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 form id="wifiCreds" action="/settings/network/wifi/add" method="post" { | ||||
|                     (PreEscaped("<!-- 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=[ssid] autofocus; | ||||
|                     (PreEscaped("<!-- input for network password -->")) | ||||
|                     input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point"; | ||||
|                     div id="buttonDiv" { | ||||
|                         input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add"; | ||||
|                         a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" } | ||||
|                     } | ||||
|                 } | ||||
|                 (PreEscaped("<!-- FLASH MESSAGE -->")) | ||||
|                 (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| /* TODO: I JUST OVERWROTE THE network_detail FUNCTION :'( :'( :'( */ | ||||
| // /settings/network/wifi | ||||
| pub fn network_detail( | ||||
|     // the ssid of the network we wish to examine in detail | ||||
|     selected: String, | ||||
|     flash_msg: Option<String>, | ||||
|     flash_name: Option<String>, | ||||
| ) -> PreEscaped<String> { | ||||
|     // retrieve network detail data | ||||
|     let context = NetworkDetailContext::build(); | ||||
|     // have credentials for the access point we're viewing previously been saved? | ||||
|     // ie. is this a known access point? | ||||
|     let selected_is_saved = context.saved_aps.contains(&selected); | ||||
|  | ||||
|     let back = "/settings/network".to_string(); | ||||
|     let title = "WiFi Network Detail".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- NETWORK DETAIL -->")) | ||||
|         // select only the access point we are interested in | ||||
|         @match context.wlan_networks.get_key_value(&selected) { | ||||
|             Some((ssid, ap)) => { | ||||
|                 @let capsule_class = if ssid == &context.wlan_ssid { | ||||
|                     "two-grid capsule success-border" | ||||
|                 } else { | ||||
|                     "two-grid capsule" | ||||
|                 }; | ||||
|                 @let ap_status = if ssid == &context.wlan_ssid { | ||||
|                     "CONNECTED" | ||||
|                 } else if ap.state == "Available" { | ||||
|                     "AVAILABLE" | ||||
|                 } else { | ||||
|                     "NOT IN RANGE" | ||||
|                 }; | ||||
|                 @let ap_protocol = match &ap.detail { | ||||
|                     Some(scan) => scan.protocol.clone(), | ||||
|                     None => "Unknown".to_string() | ||||
|                 }; | ||||
|                 @let ap_signal = match ap.signal { | ||||
|                     Some(signal) => signal.to_string(), | ||||
|                     None => "Unknown".to_string() | ||||
|                 }; | ||||
|                 (PreEscaped("<!-- NETWORK CARD -->")) | ||||
|                 div class="card center" { | ||||
|                     (PreEscaped("<!-- NETWORK INFO BOX -->")) | ||||
|                     div class=(capsule_class) title="PeachCloud network mode and status" { | ||||
|                         (PreEscaped("<!-- left column -->")) | ||||
|                         (PreEscaped("<!-- NETWORK STATUS ICON -->")) | ||||
|                         div class="grid-column-1" { | ||||
|                             img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon"; | ||||
|                             label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (ap_status) } | ||||
|                         } | ||||
|                         (PreEscaped("<!-- right column -->")) | ||||
|                         (PreEscaped("<!-- NETWORK DETAILED INFO -->")) | ||||
|                         div class="grid-column-2" { | ||||
|                             label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" } | ||||
|                             p id="netSsid" class="card-text" title="SSID" { (ssid) } | ||||
|                             label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" } | ||||
|                             p id="netSec" class="card-text" title="Security protocol in use by "{(ssid)}"" { (ap_protocol) } | ||||
|                             label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" } | ||||
|                             p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal) } | ||||
|                         } | ||||
|                     } | ||||
|                     (PreEscaped("<!-- BUTTONS -->")) | ||||
|                     div class="card-container" style="padding-top: 0;" { | ||||
|                         div id="buttonDiv" { | ||||
|                             @if context.wlan_ssid == selected { | ||||
|                                 form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" { | ||||
|                                     // hidden element: allows ssid to be sent in request | ||||
|                                     input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;"; | ||||
|                                     input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect"; | ||||
|                                 } | ||||
|                             } | ||||
|                             // If the selected access point appears in the list, | ||||
|                             // display the Modify and Forget buttons. | ||||
|                             @if context.saved_aps.contains(&selected) { | ||||
|                                 @if context.wlan_ssid != selected && ap.state == "Available" { | ||||
|                                     form id="wifiConnect" action="/settings/network/wifi/connect" method="post" { | ||||
|                                         // hidden element: allows ssid to be sent in request | ||||
|                                         input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;"; | ||||
|                                         input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect"; | ||||
|                                     } | ||||
|                                 } | ||||
|                                 a class="button button-primary center" href="/settings/network/wifi/modify/"{(ssid)}"" { "Modify" } | ||||
|                                 form id="wifiForget" action="/settings/network/wifi/forget" method="post" { | ||||
|                                     // hidden element: allows ssid to be sent in request | ||||
|                                     input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;"; | ||||
|                                     input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget"; | ||||
|                                 } | ||||
|                             } | ||||
|                             @if !selected_is_saved { | ||||
|                                 // Display the Add button if AP creds not already in saved networks list | ||||
|                                 a class="button button-primary center" href="/settings/network/wifi/add/"{(ssid)}"" { "Add" }; | ||||
|                             } | ||||
|                             a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" } | ||||
|                         } | ||||
|                         (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             // TODO: improve the styling of this | ||||
|             None => { | ||||
|                 div class="card center" { | ||||
|                     p { "Selected access point was not found in-range or in the list of saved access points" } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/network/wifi | ||||
| pub fn list_aps() -> PreEscaped<String> { | ||||
|     // retrieve network list data | ||||
|     let context = NetworkListContext::build(); | ||||
|  | ||||
|     let back = "/settings/network".to_string(); | ||||
|     let title = "WiFi Networks".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- NETWORK ACCESS POINT LIST -->")) | ||||
|         div class="card center" { | ||||
|             div class="center list-container" { | ||||
|                 ul class="list" { | ||||
|                     @if context.ap_state == *"up" { | ||||
|                         li class="list-item light-bg warning-border" { "Enable WiFi client mode to view saved and available networks." } | ||||
|                     } @else if !context.wlan_networks.is_empty() { | ||||
|                         @for (ssid, state) in context.wlan_networks { | ||||
|                             li { | ||||
|                                 @if ssid == context.wlan_ssid { | ||||
|                                     a class="list-item link primary-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" { | ||||
|                                         img id="netStatus" class="icon icon-active icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi online"; | ||||
|                                         p class="list-text" { (context.wlan_ssid) } | ||||
|                                         label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" } | ||||
|                                     } | ||||
|                                 } @else if state == "Available" { | ||||
|                                     a class="list-item link light-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" { | ||||
|                                         img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline"; | ||||
|                                         p class="list-text" { (ssid) } | ||||
|                                         label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) } | ||||
|                                     } | ||||
|                                 } @else { | ||||
|                                     a class="list-item link" href="/settings/network/wifi/ssid/"{(ssid)}"" { | ||||
|                                         img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline"; | ||||
|                                         p class="list-text" { (ssid) } | ||||
|                                         label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     li class="list-item light-bg" { "No saved or available networks found." } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/network/wifi | ||||
| pub fn modify_ap( | ||||
|     ssid: Option<String>, | ||||
|     flash_msg: Option<String>, | ||||
|     flash_name: Option<String>, | ||||
| ) -> PreEscaped<String> { | ||||
|     let back = "/settings/network".to_string(); | ||||
|     let title = "Modify WiFi Network".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->")) | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 form id="wifiModify" action="/settings/network/wifi/modify" method="post" { | ||||
|                     (PreEscaped("<!-- 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=[ssid] autofocus; | ||||
|                     (PreEscaped("<!-- input for network password -->")) | ||||
|                     input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point"; | ||||
|                     div id="buttonDiv" { | ||||
|                         input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save"; | ||||
|                         a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" } | ||||
|                     } | ||||
|                 } | ||||
|                 (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/network/dns | ||||
| pub fn configure_dns(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> { | ||||
|     // retrieve dyndns-related data | ||||
|     let context = ConfigureDNSContext::build(); | ||||
|  | ||||
|     let back = "/settings/network".to_string(); | ||||
|     let title = "Configure DNS".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- CONFIGURE DNS FORM -->")) | ||||
|         div class="card center" { | ||||
|             div class="form-container" { | ||||
|                 @if context.enable_dyndns { | ||||
|                     (PreEscaped("<!-- DYNDNS STATUS INDICATOR -->")) | ||||
|                     div id="dyndns-status-indicator" class="stack capsule{% if is_dyndns_online %} success-border{% else %} warning-border{% endif %}" { | ||||
|                         div class="stack" { | ||||
|                             @if context.is_dyndns_online { | ||||
|                                 label class="label-small font-near-black" { "Dynamic DNS is currently online." } | ||||
|                             } else { | ||||
|                                 label class="label-small font-near-black" { "Dynamic DNS is enabled but may be offline." } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 form id="configureDNS" action="/settings/network/dns" method="post" { | ||||
|                     div class="input-wrapper" { | ||||
|                         (PreEscaped("<!-- input for externaldomain -->")) | ||||
|                         label id="external_domain" class="label-small input-label font-near-black" { | ||||
|                             label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" } | ||||
|                             input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=(context.external_domain); | ||||
|                         } | ||||
|                     } | ||||
|                     div class="input-wrapper" { | ||||
|                         div { | ||||
|                             (PreEscaped("<!-- checkbox for dynds flag -->")) | ||||
|                             label class="label-small input-label font-gray" { "Enable Dynamic DNS" } | ||||
|                             input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dyndns" type="checkbox" { @if context.enable_dyndns { "checked" } }; | ||||
|                         } | ||||
|                     } | ||||
|                     div class="input-wrapper" { | ||||
|                         (PreEscaped("<!-- input for dyndns -->")) | ||||
|                         label id="cut" class="label-small input-label font-near-black" { | ||||
|                             label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" } | ||||
|                             input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(context.dyndns_subdomain) { ".dyn.peachcloud.org" } | ||||
|                         } | ||||
|                     } | ||||
|                     div id="buttonDiv" { | ||||
|                         input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save"; | ||||
|                     } | ||||
|                } | ||||
|             } | ||||
|             (PreEscaped("<!-- FLASH MESSAGE -->")) | ||||
|             (templates::snippets::flash_message(flash_msg, flash_name)) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| // /settings/network | ||||
| pub fn menu() -> PreEscaped<String> { | ||||
|     let ap_state = network::ap_state(); | ||||
|  | ||||
|     let back = "/settings".to_string(); | ||||
|     let title = "Network Settings".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- NETWORK SETTINGS CARD -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- BUTTONS -->")) | ||||
|             div id="buttons" { | ||||
|                 a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" } | ||||
|                 a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" } | ||||
|                 (PreEscaped("<!-- if ap is up, show \"Enable WiFi\" button, else show \"Deploy Access Point\" -->")) | ||||
|                 @if ap_state == *"up" { | ||||
|                     a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" } | ||||
|                 } @else { | ||||
|                     a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" } | ||||
|                 } | ||||
|                 a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" } | ||||
|                 a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										29
									
								
								peach-web-lite/src/templates/settings/scuttlebutt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,29 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::templates; | ||||
|  | ||||
| // /settings/scuttlebutt | ||||
| pub fn scuttlebutt() -> PreEscaped<String> { | ||||
|     let back = "/settings".to_string(); | ||||
|     let title = "Scuttlebutt Settings".to_string(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- SCUTTLEBUTT SETTINGS MENU -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- BUTTONS -->")) | ||||
|             div id="settingsButtons" { | ||||
|                 a id="networkKey" class="button button-primary center" href="/settings/scuttlebutt/network_key" title="Set Network Key" { "Set Network Key" } | ||||
|                 a id="replicationHops" class="button button-primary center" href="/settings/scuttlebutt/hops" title="Set Replication Hops" { "Set Replication Hops" } | ||||
|                 a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds" { "Remove Blocked Feeds" } | ||||
|                 a id="setDirectory" class="button button-primary center" href="/settings/scuttlebutt/set_directory" title="Set Database Directory" { "Set Database Directory" } | ||||
|                 a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem" { "Check Filesystem" } | ||||
|                 a id="repairFilesystem" class="button button-primary center" href="/settings/scuttlebutt/repair" title="Repair Filesystem" { "Repair Filesystem" } | ||||
|                 a id="disable" class="button button-primary center" href="/settings/scuttlebutt/disable" title="Disable Sbot" { "Disable Sbot" } | ||||
|                 a id="enable" class="button button-primary center" href="/settings/scuttlebutt/enable" title="Enable Sbot" { "Enable Sbot" } | ||||
|                 a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot" { "Restart Sbot" } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										22
									
								
								peach-web-lite/src/templates/snippets.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,22 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| pub fn flash_message(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> { | ||||
|     html! { | ||||
|         (PreEscaped("<!-- FLASH MESSAGE -->")) | ||||
|         @if flash_msg.is_some() { | ||||
|             @if flash_name == Some("success".to_string()) { | ||||
|                 div class="capsule center-text flash-message font-success" { | ||||
|                     (flash_msg.unwrap()) | ||||
|                 } | ||||
|             } @else if flash_name == Some("info".to_string()) { | ||||
|                 div class="capsule center-text flash-message font-info" { | ||||
|                     (flash_msg.unwrap()) | ||||
|                 } | ||||
|             } @else if flash_name == Some("error".to_string()) { | ||||
|                 div class="capsule center-text flash-message font-failure" { | ||||
|                     (flash_msg.unwrap()) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										496
									
								
								peach-web-lite/src/templates/status.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,496 @@ | ||||
| use maud::{html, PreEscaped}; | ||||
|  | ||||
| use crate::context::{network::NetworkStatusContext, status::StatusContext}; | ||||
| use crate::{templates, STANDALONE_MODE}; | ||||
|  | ||||
| fn ap_network_card(status: NetworkStatusContext) -> PreEscaped<String> { | ||||
|     html! { | ||||
|         (PreEscaped("<!-- NETWORK CARD -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- NETWORK INFO BOX -->")) | ||||
|             div class="capsule capsule-container success-border" { | ||||
|                 (PreEscaped("<!-- NETWORK STATUS GRID -->")) | ||||
|                 div class="two-grid" title="PeachCloud network mode and status" { | ||||
|                     (PreEscaped("<!-- top-right config icon -->")) | ||||
|                     a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" { | ||||
|                         img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure"; | ||||
|                     } | ||||
|                     (PreEscaped("<!-- left column -->")) | ||||
|                     (PreEscaped("<!-- network mode icon with label -->")) | ||||
|                     div class="grid-column-1" { | ||||
|                         img id="netModeIcon" class="center icon icon-active" src="/static/icons/router.svg" alt="WiFi router"; | ||||
|                         label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online" { "ONLINE" } | ||||
|                     } | ||||
|                     (PreEscaped("<!-- right column -->")) | ||||
|                     (PreEscaped("<!-- network mode, ssid & ip with labels -->")) | ||||
|                     div class="grid-column-2" { | ||||
|                         label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" } | ||||
|                         p id="netMode" class="card-text" title="Network Mode" { "Access Point" } | ||||
|                         label class="label-small font-gray" for="netSsid" title="Access Point SSID" { "SSID" } | ||||
|                         p id="netSsid" class="card-text" title="SSID" { (status.ap_ssid) } | ||||
|                         label class="label-small font-gray" for="netIp" title="Access Point IP Address" { "IP" } | ||||
|                         p id="netIp" class="card-text" title="IP" { (status.ap_ip) } | ||||
|                     } | ||||
|                 } | ||||
|                 (PreEscaped("<!-- horizontal dividing line -->")) | ||||
|                 hr; | ||||
|                 (PreEscaped("<!-- DEVICES AND TRAFFIC GRID -->")) | ||||
|                 div class="three-grid card-container" { | ||||
|                     // devices stack | ||||
|                     div class="stack" { | ||||
|                         img id="devices" class="icon icon-medium" title="Connected devices" src="/static/icons/devices.svg" alt="Digital devices"; | ||||
|                         div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                             label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices"; | ||||
|                         } | ||||
|                         label class="label-small font-gray" { "DEVICES" } | ||||
|                     } | ||||
|                     // download stack | ||||
|                     div class="stack" { | ||||
|                         img id="dataDownload" class="icon icon-medium" title="Download" src="/static/icons/down-arrow.svg" alt="Download"; | ||||
|                         div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                             @if let Some(traffic) = &status.ap_traffic { | ||||
|                                 label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) } | ||||
|                                 label class="label-small font-near-black" { (traffic.rx_unit) } | ||||
|                             } @else { | ||||
|                                 label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total"; | ||||
|                                 label class="label-small font-near-black" { "0" } | ||||
|                             } | ||||
|                         } | ||||
|                         label class="label-small font-gray" { "DOWNLOAD" } | ||||
|                     } | ||||
|                     // upload stack | ||||
|                     div class="stack" { | ||||
|                         img id="dataUpload" class="icon icon-medium" title="Upload" src="/static/icons/up-arrow.svg" alt="Upload"; | ||||
|                         div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                             @if let Some(traffic) = status.ap_traffic { | ||||
|                                 label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) } | ||||
|                                 label class="label-small font-near-black" { (traffic.tx_unit) } | ||||
|                             } @else { | ||||
|                                 label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total"; | ||||
|                                 label class="label-small font-near-black" { "0" } | ||||
|                             } | ||||
|                         } | ||||
|                         label class="label-small font-gray" { "UPLOAD" } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> { | ||||
|     let capsule = if status.wlan_state == *"up" { | ||||
|         "capsule capsule-container success-border" | ||||
|     } else { | ||||
|         "capsule capsule-container warning-border" | ||||
|     }; | ||||
|     html! { | ||||
|         (PreEscaped("<!-- NETWORK CARD -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- NETWORK INFO BOX -->")) | ||||
|             div class=(capsule) { | ||||
|                 @if status.wlan_state == *"up" { | ||||
|                     (PreEscaped("<!-- NETWORK STATUS GRID -->")) | ||||
|                     div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" { | ||||
|                         a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" { | ||||
|                             img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure"; | ||||
|                         } | ||||
|                         (PreEscaped("<!-- NETWORK STATUS -->")) | ||||
|                         (PreEscaped("<!-- left column -->")) | ||||
|                         (PreEscaped("<!-- network mode icon with label -->")) | ||||
|                         div class="grid-column-1" { | ||||
|                             img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online"; | ||||
|                             label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" } | ||||
|                         } | ||||
|                         div class="grid-column-2" { | ||||
|                             (PreEscaped("<!-- right column -->")) | ||||
|                             (PreEscaped("<!-- network mode, ssid & ip with labels -->")) | ||||
|                             label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" } | ||||
|                             p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" } | ||||
|                             label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" } | ||||
|                             p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) } | ||||
|                             label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" } | ||||
|                             p id="netIp" class="card-text" title="IP" { (status.wlan_ip) } | ||||
|                         } | ||||
|                     } | ||||
|                 } @else { | ||||
|                     div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" { | ||||
|                         a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" { | ||||
|                             img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure"; | ||||
|                         } | ||||
|                         div class="grid-column-1" { | ||||
|                             img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline"; | ||||
|                             label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" } | ||||
|                         } | ||||
|                         div class="grid-column-2" { | ||||
|                             (PreEscaped("<!-- right column -->")) | ||||
|                             (PreEscaped("<!-- network mode, ssid & ip with labels -->")) | ||||
|                             label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" } | ||||
|                             p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" } | ||||
|                             label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" } | ||||
|                             p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) } | ||||
|                             label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" } | ||||
|                             p id="netIp" class="card-text" title="IP" { (status.wlan_ip) } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             (PreEscaped("<!-- horizontal dividing line -->")) | ||||
|             hr; | ||||
|             (PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->")) | ||||
|             (PreEscaped("<!-- row of icons representing network statistics -->")) | ||||
|             div class="three-grid card-container" { | ||||
|                 div class="stack" { | ||||
|                     img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/icons/low-signal.svg"; | ||||
|                     div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                         label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" { | ||||
|                             @if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" } | ||||
|                         } | ||||
|                     } | ||||
|                     label class="label-small font-gray" { "SIGNAL" } | ||||
|                 } | ||||
|                 div class="stack" { | ||||
|                     img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg"; | ||||
|                     div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                         @if let Some(traffic) = &status.wlan_traffic { | ||||
|                             (PreEscaped("<!-- display wlan traffic data -->")) | ||||
|                             label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) } | ||||
|                             label class="label-small font-near-black" { (traffic.rx_unit) } | ||||
|                         } @else { | ||||
|                             (PreEscaped("<!-- no wlan traffic data to display -->")) | ||||
|                                 label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" } | ||||
|                                 label class="label-small font-near-black" { "MB" } | ||||
|                         } | ||||
|                     } | ||||
|                     label class="label-small font-gray" { "DOWNLOAD" } | ||||
|                 } | ||||
|                 div class="stack" { | ||||
|                     img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg"; | ||||
|                     div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                         @if let Some(traffic) = status.wlan_traffic { | ||||
|                             (PreEscaped("<!-- display wlan traffic data -->")) | ||||
|                             label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) } | ||||
|                             label class="label-small font-near-black" { (traffic.tx_unit) } | ||||
|                         } @else { | ||||
|                             (PreEscaped("<!-- no wlan traffic data to display -->")) | ||||
|                                 label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" } | ||||
|                                 label class="label-small font-near-black" { "MB" } | ||||
|                         } | ||||
|                     } | ||||
|                     label class="label-small font-gray" { "UPLOAD" } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * WORKS ... kinda | ||||
| fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> { | ||||
|     html! { | ||||
|         (PreEscaped("<!-- NETWORK CARD -->")) | ||||
|         div class="card center" { | ||||
|             (PreEscaped("<!-- NETWORK INFO BOX -->")) | ||||
|             @if status.wlan_state == *"up" { | ||||
|                 div class="capsule capsule-container success-border" { | ||||
|                     (PreEscaped("<!-- NETWORK STATUS GRID -->")) | ||||
|                     div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" { | ||||
|                         a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" { | ||||
|                             img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure"; | ||||
|                         } | ||||
|                         (PreEscaped("<!-- NETWORK STATUS -->")) | ||||
|                         (PreEscaped("<!-- left column -->")) | ||||
|                         (PreEscaped("<!-- network mode icon with label -->")) | ||||
|                         div class="grid-column-1" { | ||||
|                             img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online"; | ||||
|                             label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" } | ||||
|                         } | ||||
|                         div class="grid-column-2" { | ||||
|                             (PreEscaped("<!-- right column -->")) | ||||
|                             (PreEscaped("<!-- network mode, ssid & ip with labels -->")) | ||||
|                             label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" } | ||||
|                             p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" } | ||||
|                             label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" } | ||||
|                             p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) } | ||||
|                             label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" } | ||||
|                             p id="netIp" class="card-text" title="IP" { (status.wlan_ip) } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } @else { | ||||
|                 div class="capsule capsule-container warning-border" { | ||||
|                     div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" { | ||||
|                         a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" { | ||||
|                             img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure"; | ||||
|                         } | ||||
|                         div class="grid-column-1" { | ||||
|                             img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline"; | ||||
|                             label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" } | ||||
|                         } | ||||
|                         div class="grid-column-2" { | ||||
|                             (PreEscaped("<!-- right column -->")) | ||||
|                             (PreEscaped("<!-- network mode, ssid & ip with labels -->")) | ||||
|                             label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" } | ||||
|                             p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" } | ||||
|                             label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" } | ||||
|                             p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) } | ||||
|                             label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" } | ||||
|                             p id="netIp" class="card-text" title="IP" { (status.wlan_ip) } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             (PreEscaped("<!-- horizontal dividing line -->")) | ||||
|             hr; | ||||
|             (PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->")) | ||||
|             (PreEscaped("<!-- row of icons representing network statistics -->")) | ||||
|             div class="three-grid card-container" { | ||||
|                 div class="stack" { | ||||
|                     img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/icons/low-signal.svg"; | ||||
|                     div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                         label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" { | ||||
|                             @if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" } | ||||
|                         } | ||||
|                     } | ||||
|                     label class="label-small font-gray" { "SIGNAL" } | ||||
|                 } | ||||
|                 div class="stack" { | ||||
|                     img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg"; | ||||
|                     div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                         @if let Some(traffic) = &status.wlan_traffic { | ||||
|                             (PreEscaped("<!-- display wlan traffic data -->")) | ||||
|                             label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) } | ||||
|                             label class="label-small font-near-black" { (traffic.rx_unit) } | ||||
|                         } @else { | ||||
|                             (PreEscaped("<!-- no wlan traffic data to display -->")) | ||||
|                                 label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" } | ||||
|                                 label class="label-small font-near-black" { "MB" } | ||||
|                         } | ||||
|                     } | ||||
|                     label class="label-small font-gray" { "DOWNLOAD" } | ||||
|                 } | ||||
|                 div class="stack" { | ||||
|                     img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg"; | ||||
|                     div class="flex-grid" style="padding-top: 0.5rem;" { | ||||
|                         @if let Some(traffic) = status.wlan_traffic { | ||||
|                             (PreEscaped("<!-- display wlan traffic data -->")) | ||||
|                             label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) } | ||||
|                             label class="label-small font-near-black" { (traffic.tx_unit) } | ||||
|                         } @else { | ||||
|                             (PreEscaped("<!-- no wlan traffic data to display -->")) | ||||
|                                 label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" } | ||||
|                                 label class="label-small font-near-black" { "MB" } | ||||
|                         } | ||||
|                     } | ||||
|                     label class="label-small font-gray" { "UPLOAD" } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| */ | ||||
|  | ||||
| pub fn network() -> PreEscaped<String> { | ||||
|     let back = "/status".to_string(); | ||||
|     let title = "Network Status".to_string(); | ||||
|  | ||||
|     // retrieve network status data | ||||
|     let status = NetworkStatusContext::build(); | ||||
|  | ||||
|     let content = html! { | ||||
|         (PreEscaped("<!-- NETWORK STATUS -->")) | ||||
|         // if ap is up, show ap card, else show wlan card | ||||
|         @if status.ap_state == *"up" { | ||||
|             (ap_network_card(status)) | ||||
|         } @else { | ||||
|             (wlan_network_card(status)) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
|  | ||||
| fn scuttlebutt_status() -> PreEscaped<String> { | ||||
|     html! { | ||||
|         (PreEscaped("<!-- SCUTTLEBUTT STATUS -->")) | ||||
|         div class="card center" { | ||||
|             div class="card-container" { | ||||
|                 p { "Network key: " } | ||||
|                 p { "Replication hops: " } | ||||
|                 p { "Sbot version: " } | ||||
|                 p { "Process status: " } | ||||
|                 p { "Process uptime: " } | ||||
|                 p { "Blobstore size: " } | ||||
|                 p { "Latest sequence number: " } | ||||
|                 p { "Last time you visited this page, latest sequence was x ... now it's y" } | ||||
|                 p { "Number of follows / followers / friends / blocks" } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn status() -> PreEscaped<String> { | ||||
|     let back = "/".to_string(); | ||||
|     let title = if *STANDALONE_MODE { | ||||
|         "Scuttlebutt Status".to_string() | ||||
|     } else { | ||||
|         "System Status".to_string() | ||||
|     }; | ||||
|  | ||||
|     // retrieve system status data | ||||
|     let status = StatusContext::build(); | ||||
|  | ||||
|     // render the scuttlebutt status template | ||||
|     let content = if *STANDALONE_MODE { | ||||
|         scuttlebutt_status() | ||||
|     // or render the complete system status template | ||||
|     } else { | ||||
|         html! { | ||||
|             (PreEscaped("<!-- SYSTEM STATUS -->")) | ||||
|             div class="card center" { | ||||
|                 div class="card-container" { | ||||
|                     div class="three-grid" { | ||||
|                         (PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->")) | ||||
|                         (PreEscaped("<!-- Display microservice status for network, oled & stats -->")) | ||||
|                         a class="link" href="/status/network" { | ||||
|                             div class="stack capsule success-border" { | ||||
|                                 img id="networkIcon" class="icon icon-medium" alt="Network" title="Network microservice status" src="/static/icons/wifi.svg"; | ||||
|                                 div class="stack" style="padding-top: 0.5rem;" { | ||||
|                                     label class="label-small font-near-black" { "Networking" }; | ||||
|                                     label class="label-small font-near-black" { "(network_ping)" }; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         (PreEscaped("<!-- PEACH-OLED STATUS STACK -->")) | ||||
|                         div class="stack capsule success-border" { | ||||
|                             img id="oledIcon" class="icon icon-medium" alt="Display" title="OLED display microservice status" src="/static/icons/lcd.svg"; | ||||
|                             div class="stack" style="padding-top: 0.5rem;" { | ||||
|                                 label class="label-small font-near-black" { "Display" }; | ||||
|                                 label class="label-small font-near-black" { "(oled_ping)" }; | ||||
|                             } | ||||
|                         } | ||||
|                         (PreEscaped("<!-- PEACH-STATS STATUS STACK -->")) | ||||
|                         div class="stack capsule success-border" { | ||||
|                             img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/static/icons/chart.svg"; | ||||
|                             div class="stack" style="padding-top: 0.5rem;" { | ||||
|                                 label class="label-small font-near-black" { "Statistics" }; | ||||
|                                 label class="label-small font-near-black" { "AVAILABLE" }; | ||||
|                             } | ||||
|                         } | ||||
|                         (PreEscaped("<!-- DYNDNS STATUS STACK -->")) | ||||
|                         (PreEscaped("<!-- Display status for dynsdns, config & sbot -->")) | ||||
|                         div class="stack capsule success-border" { | ||||
|                             img id="networkIcon" class="icon icon-medium" alt="Dyndns" title="Dyndns status" src="/static/icons/dns.png"; | ||||
|                             div class="stack" style="padding-top: 0.5rem;" { | ||||
|                                 label class="label-small font-near-black" { "Dyn DNS" }; | ||||
|                                 label class="label-small font-near-black" { "(dns_ping)" }; | ||||
|                             } | ||||
|                         } | ||||
|                         (PreEscaped("<!-- CONFIG STATUS STACK -->")) | ||||
|                         // TODO: render capsule border according to status | ||||
|                         div class="stack capsule success-border" { | ||||
|                             img id="networkIcon" class="icon icon-medium" alt="Config" title="Config status" src="/static/icons/clipboard.png"; | ||||
|                             div class="stack" style="padding-top: 0.5rem;" { | ||||
|                                 label class="label-small font-near-black" { "Config" }; | ||||
|                                 label class="label-small font-near-black" { "(status)" }; | ||||
|                             } | ||||
|                         } | ||||
|                         (PreEscaped("<!-- SBOT STATUS STACK -->")) | ||||
|                         div class="stack capsule success-border" { | ||||
|                             img id="networkIcon" class="icon icon-medium" alt="Sbot" title="Sbot status" src="/static/icons/hermies.svg"; | ||||
|                             div class="stack" style="padding-top: 0.5rem;" { | ||||
|                                 label class="label-small font-near-black" { "Sbot" }; | ||||
|                                 label class="label-small font-near-black" { "(sbot_status)" } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 div class="card-container" { | ||||
|                     (PreEscaped("<!-- Display CPU usage meter -->")) | ||||
|                     @match status.cpu_usage_percent { | ||||
|                         Some(x) => { | ||||
|                             div class="flex-grid" { | ||||
|                                 span class="card-text" { "CPU" } | ||||
|                                 span class="label-small push-right" { (x) "%" } | ||||
|                             } | ||||
|                             meter value=(x) min="0" max="100" title="CPU usage" { | ||||
|                                 div class="meter-gauge" { | ||||
|                                     span style="width: "{(x)}"%;" { "CPU Usage" } | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         _ => p class="card-text" { "CPU usage data unavailable" } | ||||
|                     } | ||||
|                     (PreEscaped("<!-- Display memory usage meter -->")) | ||||
|                     @match status.mem_usage_percent { | ||||
|                         Some(x) => { | ||||
|                             @let (mem_free, mem_unit) = match status.mem_free { | ||||
|                                 Some(x) => { | ||||
|                                     if x > 1024 { | ||||
|                                         ((x / 1024), "GB".to_string()) | ||||
|                                     } else { | ||||
|                                         (x, "MB".to_string()) | ||||
|                                     } | ||||
|                                 }, | ||||
|                                 _ => (0, "MB".to_string()) | ||||
|                             }; | ||||
|                             @let mem_total = status.mem_total.unwrap_or(0); | ||||
|                             @let mem_used = status.mem_used.unwrap_or(0); | ||||
|                             div class="flex-grid" { | ||||
|                                 span class="card-text" { "Memory" } | ||||
|                                 span class="label-small push-right" { (x) " % ("(mem_free)" "(mem_unit)" free)" } | ||||
|                             } | ||||
|                             meter value=(mem_used) min="0" max=(mem_total) title="Memory usage" { | ||||
|                                 div class="meter-gauge" { | ||||
|                                     span style="width: "{(x)}"%;" { "Memory Usage" } | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         _ => p class="card-text" { "Memory usage data unavailable" } | ||||
|                     } | ||||
|                     (PreEscaped("<!-- Display disk usage meter -->")) | ||||
|                     @match status.disk_usage_percent { | ||||
|                         Some(x) => { | ||||
|                             // define free disk space with appropriate unit (GB if > 1024) | ||||
|                             @let (disk_free, disk_unit) = match status.disk_free { | ||||
|                                 Some(x) => { | ||||
|                                     if x > 1024 { | ||||
|                                         ((x / 1024), "GB".to_string()) | ||||
|                                     } else { | ||||
|                                         (x, "MB".to_string()) | ||||
|                                     } | ||||
|                                 }, | ||||
|                                 _ => (0, "MB".to_string()) | ||||
|                             }; | ||||
|                             div class="flex-grid" { | ||||
|                                 span class="card-text" { "Disk" }; | ||||
|                                 span class="label-small push-right" { (x) " % ("(disk_free)" "(disk_unit)")" }; | ||||
|                             } | ||||
|                             meter value=(x) min="0" max="100" title="Disk usage" { | ||||
|                                 div class="meter-gauge" { | ||||
|                                     span style="width: "{(x)}"%;" { "Disk Usage" }; | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         _ => p class="card-text" { "Disk usage data unavailable" } | ||||
|                     } | ||||
|                     (PreEscaped("<!-- Display system uptime in minutes -->")) | ||||
|                     @match status.uptime { | ||||
|                         Some(x) => { | ||||
|                             @if x < 60 { | ||||
|                                 p class="capsule center-text" { "Uptime: "(x)" minutes" } | ||||
|                             } @else { | ||||
|                                 @let hours = x / 60; | ||||
|                                 @let mins = x % 60; | ||||
|                                 p class="capsule center-text" { "Uptime: "(hours)" hours, "(mins)" minutes" } | ||||
|                             } | ||||
|                         }, | ||||
|                         _ => p class="card-text" { "Uptime data unavailable" } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     templates::base::base(back, title, content) | ||||
| } | ||||
							
								
								
									
										177
									
								
								peach-web-lite/static/css/_variables.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,177 @@ | ||||
| /* | ||||
|  | ||||
|     VARIABLES | ||||
|  | ||||
| */ | ||||
|  | ||||
| @custom-media --breakpoint-not-small screen and (min-width: 30em); | ||||
| @custom-media --breakpoint-medium screen and (min-width: 30em) and (max-width: 60em); | ||||
| @custom-media --breakpoint-large screen and (min-width: 60em); | ||||
|  | ||||
| :root { | ||||
|  | ||||
|   --sans-serif: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, helvetica, 'helvetica neue', ubuntu, roboto, noto, 'segoe ui', arial, sans-serif; | ||||
|   --serif: georgia, serif; | ||||
|   --code: consolas, monaco, monospace; | ||||
|  | ||||
|   --font-size-headline: 6rem; | ||||
|   --font-size-subheadline: 5rem; | ||||
|   --font-size-1: 3rem; | ||||
|   --font-size-2: 2.25rem; | ||||
|   --font-size-3: 1.5rem; | ||||
|   --font-size-4: 1.25rem; | ||||
|   --font-size-5: 1rem; | ||||
|   --font-size-6: .875rem; | ||||
|   --font-size-7: .75rem; | ||||
|  | ||||
|   --letter-spacing-tight:-.05em; | ||||
|   --letter-spacing-1:.1em; | ||||
|   --letter-spacing-2:.25em; | ||||
|  | ||||
|   --line-height-solid: 1; | ||||
|   --line-height-title: 1.25; | ||||
|   --line-height-copy: 1.5; | ||||
|  | ||||
|   --measure: 30em; | ||||
|   --measure-narrow: 20em; | ||||
|   --measure-wide: 34em; | ||||
|  | ||||
|   --spacing-none: 0; | ||||
|   --spacing-extra-small: .25rem; | ||||
|   --spacing-small: .5rem; | ||||
|   --spacing-medium: 1rem; | ||||
|   --spacing-large: 2rem; | ||||
|   --spacing-extra-large: 4rem; | ||||
|   --spacing-extra-extra-large: 8rem; | ||||
|   --spacing-extra-extra-extra-large: 16rem; | ||||
|   --spacing-copy-separator: 1.5em; | ||||
|  | ||||
|   --height-1: 1rem; | ||||
|   --height-2: 2rem; | ||||
|   --height-3: 4rem; | ||||
|   --height-4: 8rem; | ||||
|   --height-5: 16rem; | ||||
|  | ||||
|   --width-1: 1rem; | ||||
|   --width-2: 2rem; | ||||
|   --width-3: 4rem; | ||||
|   --width-4: 8rem; | ||||
|   --width-5: 16rem; | ||||
|  | ||||
|   --max-width-1: 1rem; | ||||
|   --max-width-2: 2rem; | ||||
|   --max-width-3: 4rem; | ||||
|   --max-width-4: 8rem; | ||||
|   --max-width-5: 16rem; | ||||
|   --max-width-6: 32rem; | ||||
|   --max-width-7: 48rem; | ||||
|   --max-width-8: 64rem; | ||||
|   --max-width-9: 96rem; | ||||
|  | ||||
|   --border-radius-none: 0; | ||||
|   --border-radius-1: .125rem; | ||||
|   --border-radius-2: .25rem; | ||||
|   --border-radius-3: .5rem; | ||||
|   --border-radius-4: 1rem; | ||||
|   --border-radius-circle: 100%; | ||||
|   --border-radius-pill: 9999px; | ||||
|  | ||||
|   --border-width-none: 0; | ||||
|   --border-width-1: .125rem; | ||||
|   --border-width-2: .25rem; | ||||
|   --border-width-3: .5rem; | ||||
|   --border-width-4: 1rem; | ||||
|   --border-width-5: 2rem; | ||||
|  | ||||
|   --box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 ); | ||||
|   --box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 ); | ||||
|   --box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 ); | ||||
|   --box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 ); | ||||
|   --box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 ); | ||||
|  | ||||
|   --black: #000; | ||||
|   --near-black: #111; | ||||
|   --dark-gray:#333; | ||||
|   --mid-gray:#555; | ||||
|   --gray: #777; | ||||
|   --silver: #999; | ||||
|   --light-silver: #aaa; | ||||
|   --moon-gray: #ccc; | ||||
|   --light-gray: #eee; | ||||
|   --near-white: #f4f4f4; | ||||
|   --white: #fff; | ||||
|  | ||||
|   --transparent: transparent; | ||||
|  | ||||
|   --black-90: rgba(0,0,0,.9); | ||||
|   --black-80: rgba(0,0,0,.8); | ||||
|   --black-70: rgba(0,0,0,.7); | ||||
|   --black-60: rgba(0,0,0,.6); | ||||
|   --black-50: rgba(0,0,0,.5); | ||||
|   --black-40: rgba(0,0,0,.4); | ||||
|   --black-30: rgba(0,0,0,.3); | ||||
|   --black-20: rgba(0,0,0,.2); | ||||
|   --black-10: rgba(0,0,0,.1); | ||||
|   --black-05: rgba(0,0,0,.05); | ||||
|   --black-025: rgba(0,0,0,.025); | ||||
|   --black-0125: rgba(0,0,0,.0125); | ||||
|  | ||||
|   --white-90: rgba(255,255,255,.9); | ||||
|   --white-80: rgba(255,255,255,.8); | ||||
|   --white-70: rgba(255,255,255,.7); | ||||
|   --white-60: rgba(255,255,255,.6); | ||||
|   --white-50: rgba(255,255,255,.5); | ||||
|   --white-40: rgba(255,255,255,.4); | ||||
|   --white-30: rgba(255,255,255,.3); | ||||
|   --white-20: rgba(255,255,255,.2); | ||||
|   --white-10: rgba(255,255,255,.1); | ||||
|   --white-05: rgba(255,255,255,.05); | ||||
|   --white-025: rgba(255,255,255,.025); | ||||
|   --white-0125: rgba(255,255,255,.0125); | ||||
|  | ||||
|   --dark-red:  #e7040f; | ||||
|   --red:  #ff4136; | ||||
|   --light-red:  #ff725c; | ||||
|   --orange:  #ff6300; | ||||
|   --gold:  #ffb700; | ||||
|   --yellow:  #ffd700; | ||||
|   --light-yellow:  #fbf1a9; | ||||
|   --purple:  #5e2ca5; | ||||
|   --light-purple:  #a463f2; | ||||
|   --dark-pink:  #d5008f; | ||||
|   --hot-pink: #ff41b4; | ||||
|   --pink:  #ff80cc; | ||||
|   --light-pink:  #ffa3d7; | ||||
|   --dark-green:  #137752; | ||||
|   --green:  #19a974; | ||||
|   --light-green:  #9eebcf; | ||||
|   --navy:  #001b44; | ||||
|   --dark-blue:  #00449e; | ||||
|   --blue:  #357edd; | ||||
|   --light-blue:  #96ccff; | ||||
|   --lightest-blue:  #cdecff; | ||||
|   --washed-blue:  #f6fffe; | ||||
|   --washed-green:  #e8fdf5; | ||||
|   --washed-yellow:  #fffceb; | ||||
|   --washed-red:  #ffdfdf; | ||||
|  | ||||
| /* PEACHCLOUD-SPECIFIC VARIABLES */ | ||||
|  | ||||
|   --primary: var(--light-green); | ||||
|   --secondary: var(--near-white); | ||||
|   --success: var(--green); | ||||
|   --info: var(--blue); | ||||
|   --warning: var(--orange); | ||||
|   --danger: var(--red); | ||||
|   --light: var(--light-gray); | ||||
|   --dark: var(--near-black); | ||||
|  | ||||
| /* we need to add shades for each accent colour | ||||
|  * | ||||
|  * --info-100 | ||||
|  * --info-200 | ||||
|  * --info-300 -> var(--blue) | ||||
|  * --info-400 | ||||
|  * --info-500 | ||||
|  */ | ||||
| } | ||||
							
								
								
									
										971
									
								
								peach-web-lite/static/css/peachcloud.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,971 @@ | ||||
| @import url('/static/css/_variables.css'); | ||||
|  | ||||
| /* ------------------------------ *\ | ||||
|  * peachcloud.css | ||||
|  * | ||||
|  * Index | ||||
|  * - ALIGNMENT | ||||
|  * - BODY | ||||
|  * - BUTTONS | ||||
|  * - CARDS | ||||
|  * - CAPSULES | ||||
|  * - CIRCLES | ||||
|  * - COLORS | ||||
|  * - GRIDS | ||||
|  * - HTML | ||||
|  * - FLASH MESSAGE | ||||
|  * - FONTS | ||||
|  * - ICONS | ||||
|  * - INPUTS | ||||
|  * - LABELS | ||||
|  * - LINKS | ||||
|  * - LISTS | ||||
|  * - MAIN | ||||
|  * - METERS | ||||
|  * - NAVIGATION | ||||
|  * - PARAGRAPHS | ||||
|  * - SWITCHES / SLIDERS | ||||
|  * | ||||
| \* ------------------------------ */ | ||||
|  | ||||
| /* | ||||
|  * ALIGNMENT | ||||
|  */ | ||||
|  | ||||
| .center { | ||||
|     display: block; | ||||
|     margin-left: auto; | ||||
|     margin-right: auto; | ||||
| } | ||||
|  | ||||
| .center-text { | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .center-vert { | ||||
|     position: absolute; | ||||
|     top: 50%; | ||||
|     transform: translateY(-50%); | ||||
| } | ||||
|  | ||||
| .push-right { | ||||
|     margin-left: auto; | ||||
|     padding-right: 0; | ||||
| } | ||||
|  | ||||
| .top-left { | ||||
|     /* place-self combines align-self and justify-self */ | ||||
|     place-self: end center; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .top-left { | ||||
|         place-self: end; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .top-right { | ||||
|     place-self: end center; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .top-right { | ||||
|         place-self: end start; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .top-middle { | ||||
|     place-self: center; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .top-middle { | ||||
|         padding-bottom: 2rem; | ||||
|         place-self: center; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .middle { | ||||
|     place-self: center; | ||||
|     grid-column-start: 1; | ||||
|     grid-column-end: 4; | ||||
| } | ||||
|  | ||||
| .bottom-middle { | ||||
|     place-self: center; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .bottom-middle { | ||||
|         padding-top: 2rem; | ||||
|         place-self: center; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .bottom-left { | ||||
|     place-self: start center; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .bottom-left { | ||||
|         place-self: start end; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .bottom-right { | ||||
|     place-self: start center; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .bottom-right { | ||||
|         place-self: start; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * BODY | ||||
|  */ | ||||
|  | ||||
| body { | ||||
|   background-color: var(--moon-gray); | ||||
|   height: 100%; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * BUTTONS | ||||
|  */ | ||||
|  | ||||
| .button { | ||||
|     border: 1px solid var(--near-black); | ||||
|     border-radius: var(--border-radius-2); | ||||
|     /* Needed to render inputs & buttons of equal width */ | ||||
|     -moz-box-sizing: border-box; | ||||
|     -webkit-box-sizing: border-box; | ||||
|     box-sizing: border-box; | ||||
|     color: var(--near-black); | ||||
|     cursor: pointer; | ||||
|     padding: 10px; | ||||
|     text-align: center; | ||||
|     text-decoration: none; | ||||
|     font-size: var(--font-size-5); | ||||
|     font-family: var(--sans-serif); | ||||
|     width: 80%; | ||||
|     margin-top: 5px; | ||||
|     margin-bottom: 5px; | ||||
| } | ||||
|  | ||||
| .button.full-width { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| .button-div { | ||||
|     grid-column-start: 1; | ||||
|     grid-column-end: 4; | ||||
|     margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .button-primary { | ||||
|     background-color: var(--light-gray); | ||||
| } | ||||
|  | ||||
| .button-primary:hover { | ||||
|     background-color: var(--primary); | ||||
| } | ||||
|  | ||||
| .button-primary:focus { | ||||
|     background-color: var(--primary); | ||||
|     outline: none; | ||||
| } | ||||
|  | ||||
| .button-secondary { | ||||
|     background-color: var(--light-gray); | ||||
| } | ||||
|  | ||||
| .button-secondary:hover { | ||||
|     background-color: var(--light-silver); | ||||
| } | ||||
|  | ||||
| .button-secondary:focus { | ||||
|     background-color: var(--light-silver); | ||||
|     outline: none; | ||||
| } | ||||
|  | ||||
| .button-warning { | ||||
|     background-color: var(--light-gray); | ||||
| } | ||||
|  | ||||
| .button-warning:hover { | ||||
|     background-color: var(--light-red); | ||||
| } | ||||
|  | ||||
| .button-warning:focus { | ||||
|     background-color: var(--light-red); | ||||
|     outline: none; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * CAPSULES | ||||
|  */ | ||||
|  | ||||
| .capsule { | ||||
|     padding: 1rem; | ||||
|     border: var(--border-width-1) solid; | ||||
|     border-radius: var(--border-radius-3); | ||||
|     background-color: var(--light-gray); | ||||
|     /* margin-top: 1rem; */ | ||||
|     /* margin-bottom: 1rem; */ | ||||
| } | ||||
|  | ||||
| .capsule-container { | ||||
|     margin-left: 1rem; | ||||
|     margin-right: 1rem; | ||||
|     padding-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .capsule-container { | ||||
|       margin-left: 0; | ||||
|       margin-right: 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * CARDS | ||||
|  */ | ||||
|  | ||||
| .card { | ||||
|     min-height: 50vh; | ||||
|     max-height: 90vh; | ||||
|     position: relative; | ||||
|     width: 100%; | ||||
|     margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| @media only screen and (min-width: 600px) { | ||||
|     .card { | ||||
|         min-height: 50vh; | ||||
|         max-height: 90vh; | ||||
|         width: 20rem; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .card-container { | ||||
|     justify-content: center; | ||||
|     padding: 0.5rem; | ||||
| } | ||||
|  | ||||
| .form-container { | ||||
|     justify-content: center; | ||||
|     padding-top: 1rem; | ||||
|     padding-bottom: 1rem; | ||||
|     width: 80%; | ||||
|     margin: auto; | ||||
| } | ||||
|  | ||||
| .text-container { | ||||
|     width: 80%; | ||||
|     margin: auto; | ||||
| } | ||||
|  | ||||
| .card-text { | ||||
|     margin: 0; | ||||
|     font-size: var(--font-size-5); | ||||
|     padding-bottom: 0.3rem; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|     display: grid; | ||||
|     grid-template-columns: 2fr 5fr 2fr; | ||||
|     grid-template-rows: auto; | ||||
|     grid-row-gap: 1rem; | ||||
|     align-items: center; | ||||
|     justify-items: center; | ||||
|     margin-bottom: 1rem; | ||||
|     margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * CIRCLES | ||||
|  */ | ||||
|  | ||||
| .circle { | ||||
|     align-items: center; | ||||
|     background: var(--light-gray); | ||||
|     border-radius: 50%; | ||||
|     box-shadow: var(--box-shadow-3); | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .circle-small { | ||||
|     height: 5rem; | ||||
|     width: 5rem; | ||||
| } | ||||
|  | ||||
| .circle-medium { | ||||
|     height: 8rem; | ||||
|     width: 8rem; | ||||
| } | ||||
|  | ||||
| .circle-large { | ||||
|     height: 13rem; | ||||
|     width: 13rem; | ||||
| } | ||||
|  | ||||
| .circle-success { | ||||
|     background-color: var(--success); | ||||
|     color: var(--white); | ||||
|     font-size: var(--font-size-4); | ||||
| } | ||||
|  | ||||
| .circle-warning { | ||||
|     background-color: var(--warning); | ||||
|     color: var(--white); | ||||
|     font-size: var(--font-size-4); | ||||
| } | ||||
|  | ||||
| .circle-error { | ||||
|     background-color: var(--danger); | ||||
|     color: var(--white); | ||||
|     font-size: var(--font-size-4); | ||||
| } | ||||
|  | ||||
| /* quartered-circle: circle for the center of radial-menu */ | ||||
|  | ||||
| .quartered-circle { | ||||
|     width: 100px; | ||||
|     height: 100px; | ||||
| } | ||||
|  | ||||
| .quarter { | ||||
|     width: 50%; | ||||
|     height: 50%; | ||||
| } | ||||
|  | ||||
| .quarter-link { | ||||
|     left: 50%; | ||||
|     margin: -2em; | ||||
|     top: 50%; | ||||
| } | ||||
|  | ||||
| .quarter-icon { | ||||
|     position: absolute; | ||||
|     bottom: 1em; | ||||
|     left: 1.5em; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * COLORS | ||||
|  */ | ||||
|  | ||||
| .primary-bg { | ||||
|     background-color: var(--primary); | ||||
| } | ||||
|  | ||||
| .secondary-bg { | ||||
|     background-color: var(--secondary); | ||||
| } | ||||
|  | ||||
| .success-bg { | ||||
|     background-color: var(--success); | ||||
| } | ||||
|  | ||||
| .info-bg { | ||||
|     background-color: var(--info); | ||||
| } | ||||
|  | ||||
| .warning-bg { | ||||
|     background-color: var(--warning); | ||||
| } | ||||
|  | ||||
| .danger-bg { | ||||
|     background-color: var(--danger); | ||||
| } | ||||
|  | ||||
| .light-bg { | ||||
|     background-color: var(--light); | ||||
| } | ||||
|  | ||||
| .primary-border { | ||||
|     border-color: var(--primary); | ||||
| } | ||||
|  | ||||
| .success-border { | ||||
|     border-color: var(--success); | ||||
| } | ||||
|  | ||||
| .info-border { | ||||
|     border-color: var(--info); | ||||
| } | ||||
|  | ||||
| .warning-border { | ||||
|     border-color: var(--warning); | ||||
| } | ||||
|  | ||||
| .danger-border { | ||||
|     border-color: var(--danger); | ||||
| } | ||||
|  | ||||
| .dark-gray-border { | ||||
|     border-color: var(--dark-gray); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * GRIDS | ||||
|  */ | ||||
|  | ||||
| .grid { | ||||
|     display: grid; | ||||
|     grid-template-columns: 2fr 1fr 2fr; | ||||
|     grid-template-rows: 2fr 1fr 2fr; | ||||
|     height: 80vh; | ||||
| } | ||||
|  | ||||
| .flex-grid { | ||||
|     display: flex; | ||||
|     align-content: space-between; | ||||
|     align-items: baseline; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .two-grid { | ||||
|     display: grid; | ||||
|     grid-template-columns: repeat(2, 1fr); | ||||
|     grid-template-rows: auto; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     justify-items: center; | ||||
|     padding-bottom: 1rem; | ||||
|     /* margin-right: 2rem; */ | ||||
|     /* margin-left: 2rem; */ | ||||
|     /* padding-top: 1.5rem; */ | ||||
| } | ||||
|  | ||||
| .two-grid-top-right { | ||||
|     grid-column: 2; | ||||
|     justify-self: right; | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| .three-grid { | ||||
|     display: grid; | ||||
|     grid-template-columns: repeat(3, 1fr); | ||||
|     grid-template-rows: auto; | ||||
|     grid-gap: 10px; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .profile-grid { | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr 2fr; | ||||
|     grid-template-rows: auto; | ||||
|     grid-gap: 10px; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     justify-items: center; | ||||
|     margin-right: 2rem; | ||||
|     margin-left: 2rem; | ||||
|     padding-top: 1.5rem; | ||||
|     padding-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .stack { | ||||
|     display: grid; | ||||
|     align-items: flex-end; | ||||
|     justify-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .three-grid-icon-1 { | ||||
|     align-self: center; | ||||
|     grid-column: 1; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
|     margin-bottom: 10px; | ||||
|     max-width: 55%; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .three-grid-icon-2 { | ||||
|     align-self: center; | ||||
|     grid-column: 2; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
|     margin-bottom: 10px; | ||||
|     max-width: 55%; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .three-grid-icon-3 { | ||||
|     align-self: center; | ||||
|     grid-column: 3; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
|     margin-bottom: 10px; | ||||
|     max-width: 55%; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .three-grid-label-1 { | ||||
|     align-self: center; | ||||
|     grid-column: 1; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .three-grid-label-2 { | ||||
|     align-self: center; | ||||
|     grid-column: 2; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .three-grid-label-3 { | ||||
|     align-self: center; | ||||
|     grid-column: 3; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .grid-column-1 { | ||||
|     grid-column: 1; | ||||
| } | ||||
|  | ||||
| .grid-column-2 { | ||||
|     grid-column: 2; | ||||
|     justify-self: left; | ||||
| } | ||||
|  | ||||
| .grid-column-3 { | ||||
|     grid-column: 3; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * HTML | ||||
|  */ | ||||
|  | ||||
| html { | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * FLASH MESSAGE | ||||
|  */ | ||||
|  | ||||
| .flash-message { | ||||
|     font-family: var(--sans-serif); | ||||
|     font-size: var(--font-size-6); | ||||
|     margin-left: 2rem; | ||||
|     margin-right: 2rem; | ||||
|     margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * FONTS | ||||
|  */ | ||||
|  | ||||
| .font-near-black { | ||||
|     color: var(--near-black); | ||||
| } | ||||
|  | ||||
| .font-gray { | ||||
|     color: var(--mid-gray); | ||||
| } | ||||
|  | ||||
| .font-light-gray { | ||||
|     color: var(--silver); | ||||
| } | ||||
|  | ||||
| .font-success { | ||||
|     color: var(--success); | ||||
| } | ||||
|  | ||||
| .font-warning { | ||||
|     color: var(--warning); | ||||
| } | ||||
|  | ||||
| .font-failure { | ||||
|     color: var(--danger); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * ICONS | ||||
|  */ | ||||
|  | ||||
| .icon { | ||||
|     width: 3rem; | ||||
| } | ||||
|  | ||||
| .icon-small { | ||||
|     width: 1rem; | ||||
| } | ||||
|  | ||||
| .icon-medium { | ||||
|     width: 2rem; | ||||
| } | ||||
|  | ||||
| .icon-large { | ||||
|     width: 5rem; | ||||
| } | ||||
|  | ||||
| .icon-100 { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| /* icon-active: sets color of icon svg to near-black */ | ||||
| .icon-active { | ||||
|     filter: invert(0%) sepia(1%) saturate(4171%) hue-rotate(79deg) brightness(86%) contrast(87%); | ||||
| } | ||||
|  | ||||
| /* icon-inactive: sets color of icon svg to gray */ | ||||
| .icon-inactive { | ||||
|     filter: invert(72%) sepia(8%) saturate(14%) hue-rotate(316deg) brightness(93%) contrast(92%); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * INPUTS | ||||
|  */ | ||||
|  | ||||
| .input { | ||||
|     /* Needed to render inputs & buttons of equal width */ | ||||
|     -moz-box-sizing: border-box; | ||||
|     -webkit-box-sizing: border-box; | ||||
|     box-sizing: border-box; | ||||
|     margin-top: 0.5rem; | ||||
|     margin-bottom: 1rem; | ||||
|     padding-left: 5px; | ||||
|     line-height: 1.5rem; | ||||
|     width: 80%; | ||||
| } | ||||
|  | ||||
| .form-input { | ||||
|     margin-bottom: 0; | ||||
|     margin-left: 0px; | ||||
|     border: 0px; | ||||
|     padding-left: 5px; | ||||
|     line-height: 1.5rem; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| .message-input { | ||||
|     height: 7rem; | ||||
|     overflow: auto; | ||||
|     resize: vertical; | ||||
| } | ||||
|  | ||||
| .alert-input { | ||||
|     /* Needed to render inputs & buttons of equal width */ | ||||
|     -moz-box-sizing: border-box; | ||||
|     -webkit-box-sizing: border-box; | ||||
|     box-sizing: border-box; | ||||
|     margin-right: 0.25rem; | ||||
|     padding-right: 0.25rem; | ||||
|     text-align: right; | ||||
|     width: 7rem; | ||||
| } | ||||
|  | ||||
| .input-wrapper { | ||||
|     margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * LABELS | ||||
|  */ | ||||
|  | ||||
| .label-small { | ||||
|     font-family: var(--sans-serif); | ||||
|     font-size: var(--font-size-7); | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .label-medium { | ||||
|     font-size: var(--font-size-3); | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .label-large { | ||||
|     font-size: var(--font-size-2); | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .label-ellipsis { | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     width: 10rem; | ||||
| } | ||||
|  | ||||
| .input-label { | ||||
|     margin-bottom: 0.4rem; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * LINKS | ||||
|  */ | ||||
|  | ||||
| .link { | ||||
|     text-decoration: none; | ||||
|     color: var(--font-near-black); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * LISTS | ||||
|  */ | ||||
|  | ||||
| .list { | ||||
|     padding-left: 0; | ||||
|     margin-left: 0; | ||||
|     max-width: var(--max-width-6); | ||||
|     border: 1px solid var(--light-silver); | ||||
|     border-radius: var(--border-radius-2); | ||||
|     list-style-type: none; | ||||
|     font-family: var(--sans-serif); | ||||
| } | ||||
|  | ||||
| .list-container { | ||||
|     width: var(--max-width-5); | ||||
| } | ||||
|  | ||||
| .list-icon { | ||||
|     align-self: center; | ||||
|     justify-self: right; | ||||
|     grid-column: 2; | ||||
|     grid-row: 1/3; | ||||
| } | ||||
|  | ||||
| .list-item { | ||||
|     display: grid; | ||||
|     padding: 1rem; | ||||
|     border-bottom-color: var(--light-silver); | ||||
|     border-bottom-style: solid; | ||||
|     border-bottom-width: 1px; | ||||
| } | ||||
|  | ||||
| .list-text { | ||||
|     justify-self: left; | ||||
|     grid-column: 1; | ||||
|     grid-row: 1; | ||||
|     margin: 0; | ||||
|     font-size: var(--font-size-5); | ||||
| } | ||||
|  | ||||
| .list-label { | ||||
|     justify-self: left; | ||||
|     grid-column: 1; | ||||
|     grid-row: 2; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * MAIN | ||||
|  */ | ||||
|  | ||||
| main { | ||||
|   flex: 1 0 auto; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * METERS | ||||
|  */ | ||||
|  | ||||
| meter { | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 3px; | ||||
|     display: block; | ||||
|     /* height: 1rem; */ | ||||
|     margin: 0 auto; | ||||
|     margin-bottom: 1rem; | ||||
|     width: 100%; | ||||
|     /* remove default styling */ | ||||
|     -webkit-appearance: none; | ||||
|        -moz-appearance: none; | ||||
|             appearance: none; | ||||
|  | ||||
|     /* Firefox */ | ||||
|     background: none; /* remove default background */ | ||||
|     background-color: var(--near-white); | ||||
|     box-shadow: 0 5px 5px -5px #333 inset; | ||||
| } | ||||
|  | ||||
| meter::-webkit-meter-bar { | ||||
|     background: none; /* remove default background */ | ||||
|     background-color: var(--near-white); | ||||
|     box-shadow: 0 5px 5px -5px #333 inset; | ||||
| } | ||||
|  | ||||
| meter::-webkit-meter-optimum-value { | ||||
|     background-size: 100% 100%; | ||||
|     box-shadow: 0 5px 5px -5px #999 inset; | ||||
|     transition: width .5s; | ||||
| } | ||||
|  | ||||
| /* Firefox styling */ | ||||
| meter::-moz-meter-bar { | ||||
|     background: var(--mid-gray); | ||||
|     background-size: 100% 100%; | ||||
|     box-shadow: 0 5px 5px -5px #999 inset; | ||||
| } | ||||
|  | ||||
| .meter-gauge { | ||||
|     background-color: var(--near-white); | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 3px; | ||||
|     box-shadow: 0 5px 5px -5px #333 inset; | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| /* Chrome styling */ | ||||
| .meter-gauge > span { | ||||
|     background: var(--mid-gray); | ||||
|     background-size: 100% 100%; | ||||
|     box-shadow: 0 5px 5px -5px #999 inset; | ||||
|     display: block; | ||||
|     height: inherit; | ||||
|     text-indent: -9999px; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * NAVIGATION | ||||
|  */ | ||||
|  | ||||
| .nav-bar { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     width: 100%; | ||||
|     height: 2em; | ||||
|     padding-top: 1rem; | ||||
|     padding-bottom: 1rem; | ||||
|     justify-content: space-between; | ||||
| } | ||||
|  | ||||
| .nav-title { | ||||
|     font-family: var(--sans-serif); | ||||
|     font-size: var(--font-size-4); | ||||
|     font-weight: normal; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .nav-icon { | ||||
|     width: auto; | ||||
|     height: 90%; | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .nav-icon-left { | ||||
|     float: left; | ||||
|     padding-left: 10px; | ||||
| } | ||||
|  | ||||
| .nav-icon-right { | ||||
|     float: right; | ||||
|     padding-right: 10px; | ||||
| } | ||||
|  | ||||
| .nav-item { | ||||
|     display: inline-block; | ||||
|     list-style-type: none; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * PARAGRAPHS | ||||
|  */ | ||||
|  | ||||
| p { | ||||
|     font-family: var(--sans-serif); | ||||
|     overflow-wrap: anywhere; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * SWITCHES / SLIDERS | ||||
|  */ | ||||
|  | ||||
| /* switch: the box around the slider */ | ||||
| .switch { | ||||
|     display: inline-block; | ||||
|     height: 34px; | ||||
|     position: relative; | ||||
|     width: 60px; | ||||
| } | ||||
|  | ||||
| /* hide default HTML checkbox */ | ||||
| .switch input { | ||||
|     height: 0; | ||||
|     opacity: 0; | ||||
|     width: 0; | ||||
| } | ||||
|  | ||||
| .switch-icon-left { | ||||
|     align-self: center; | ||||
|     grid-column: 1; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
| } | ||||
|  | ||||
| .switch-icon-right { | ||||
|     align-self: center; | ||||
|     grid-column: 3; | ||||
|     grid-row: 1; | ||||
|     justify-self: center; | ||||
| } | ||||
|  | ||||
| .slider { | ||||
|     background-color: var(--moon-gray); | ||||
|     bottom: 0; | ||||
|     cursor: pointer; | ||||
|     left: 0; | ||||
|     position: absolute; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     transition: .4s; | ||||
|     -webkit-transition: .4s; | ||||
| } | ||||
|  | ||||
| .slider:before { | ||||
|     background-color: var(--white); | ||||
|     bottom: 4px; | ||||
|     content: ""; | ||||
|     height: 26px; | ||||
|     left: 4px; | ||||
|     position: absolute; | ||||
|     transition: .4s; | ||||
|     -webkit-transition: .4s; | ||||
|     width: 26px; | ||||
| } | ||||
|  | ||||
| input:checked + .slider { | ||||
|     background-color: var(--near-black); | ||||
| } | ||||
|  | ||||
| input:focus + .slider { | ||||
|     box-shadow: 0 0 1px var(--near-black); | ||||
| } | ||||
|  | ||||
| input:checked + .slider:before { | ||||
|     -ms-transform: translateX(26px); | ||||
|     transform: translateX(26px); | ||||
|     -webkit-transform: translateX(26px); | ||||
| } | ||||
|  | ||||
| .slider.round { | ||||
|     border-radius: 34px; | ||||
| } | ||||
|  | ||||
| .slider.round:before { | ||||
|     border-radius: 50%; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * TITLES | ||||
|  */ | ||||
|  | ||||
| .title-medium { | ||||
|     font-size: var(--font-size-4); | ||||
|     font-family: var(--sans-serif); | ||||
|     max-width: var(--max-width-6); | ||||
| } | ||||
							
								
								
									
										1
									
								
								peach-web-lite/static/hi.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | ||||
| yo :) | ||||
							
								
								
									
										56
									
								
								peach-web-lite/static/icons/alert.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,56 @@ | ||||
| <?xml version="1.0" encoding="iso-8859-1"?> | ||||
| <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve"> | ||||
| <g> | ||||
| 	<g> | ||||
| 		<path d="M503.839,395.379l-195.7-338.962C297.257,37.569,277.766,26.315,256,26.315c-21.765,0-41.257,11.254-52.139,30.102 | ||||
| 			L8.162,395.378c-10.883,18.85-10.883,41.356,0,60.205c10.883,18.849,30.373,30.102,52.139,30.102h391.398 | ||||
| 			c21.765,0,41.256-11.254,52.14-30.101C514.722,436.734,514.722,414.228,503.839,395.379z M477.861,440.586 | ||||
| 			c-5.461,9.458-15.241,15.104-26.162,15.104H60.301c-10.922,0-20.702-5.646-26.162-15.104c-5.46-9.458-5.46-20.75,0-30.208 | ||||
| 			L229.84,71.416c5.46-9.458,15.24-15.104,26.161-15.104c10.92,0,20.701,5.646,26.161,15.104l195.7,338.962 | ||||
| 			C483.321,419.836,483.321,431.128,477.861,440.586z"/> | ||||
| 	</g> | ||||
| </g> | ||||
| <g> | ||||
| 	<g> | ||||
| 		<rect x="241.001" y="176.01" width="29.996" height="149.982"/> | ||||
| 	</g> | ||||
| </g> | ||||
| <g> | ||||
| 	<g> | ||||
| 		<path d="M256,355.99c-11.027,0-19.998,8.971-19.998,19.998s8.971,19.998,19.998,19.998c11.026,0,19.998-8.971,19.998-19.998 | ||||
| 			S267.027,355.99,256,355.99z"/> | ||||
| 	</g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										39
									
								
								peach-web-lite/static/icons/back.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,39 @@ | ||||
| <?xml version="1.0" encoding="iso-8859-1"?> | ||||
| <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 viewBox="0 0 477.175 477.175" style="enable-background:new 0 0 477.175 477.175;" xml:space="preserve"> | ||||
| <g> | ||||
| 	<path d="M145.188,238.575l215.5-215.5c5.3-5.3,5.3-13.8,0-19.1s-13.8-5.3-19.1,0l-225.1,225.1c-5.3,5.3-5.3,13.8,0,19.1l225.1,225 | ||||
| 		c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4c5.3-5.3,5.3-13.8,0-19.1L145.188,238.575z"/> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 768 B | 
							
								
								
									
										7
									
								
								peach-web-lite/static/icons/book.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generated by IcoMoon.io --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20"> | ||||
| <path fill="#000000" d="M14.5 18h-10c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h10c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path> | ||||
| <path fill="#000000" d="M16.5 3c-0.276 0-0.5 0.224-0.5 0.5v15c0 0.276-0.224 0.5-0.5 0.5h-11c-0.827 0-1.5-0.673-1.5-1.5s0.673-1.5 1.5-1.5h9c0.827 0 1.5-0.673 1.5-1.5v-12c0-0.827-0.673-1.5-1.5-1.5h-10c-0.827 0-1.5 0.673-1.5 1.5v15c0 1.378 1.122 2.5 2.5 2.5h11c0.827 0 1.5-0.673 1.5-1.5v-15c0-0.276-0.224-0.5-0.5-0.5zM3.5 2h10c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5h-9c-0.562 0-1.082 0.187-1.5 0.501v-13.001c0-0.276 0.224-0.5 0.5-0.5z"></path> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 916 B | 
							
								
								
									
										9
									
								
								peach-web-lite/static/icons/chart.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generated by IcoMoon.io --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20"> | ||||
| <path fill="#000000" d="M17.5 20h-16c-0.827 0-1.5-0.673-1.5-1.5v-16c0-0.827 0.673-1.5 1.5-1.5h16c0.827 0 1.5 0.673 1.5 1.5v16c0 0.827-0.673 1.5-1.5 1.5zM1.5 2c-0.276 0-0.5 0.224-0.5 0.5v16c0 0.276 0.224 0.5 0.5 0.5h16c0.276 0 0.5-0.224 0.5-0.5v-16c0-0.276-0.224-0.5-0.5-0.5h-16z"></path> | ||||
| <path fill="#000000" d="M6.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-9c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v9c0 0.276-0.224 0.5-0.5 0.5zM5 16h1v-8h-1v8z"></path> | ||||
| <path fill="#000000" d="M10.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-12c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5zM9 16h1v-11h-1v11z"></path> | ||||
| <path fill="#000000" d="M14.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-5c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v5c0 0.276-0.224 0.5-0.5 0.5zM13 16h1v-4h-1v4z"></path> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								peach-web-lite/static/icons/clipboard.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.1 KiB | 
							
								
								
									
										45
									
								
								peach-web-lite/static/icons/cloud-disconnected.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,45 @@ | ||||
| <?xml version="1.0" encoding="iso-8859-1"?> | ||||
| <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 width="612px" height="612px" viewBox="0 0 612 612" style="enable-background:new 0 0 612 612;" xml:space="preserve"> | ||||
| <g> | ||||
| 	<g id="cloud-off"> | ||||
| 		<path d="M494.7,229.5c-17.851-86.7-94.351-153-188.7-153c-38.25,0-73.95,10.2-102,30.6l38.25,38.25 | ||||
| 			c17.85-12.75,40.8-17.85,63.75-17.85c76.5,0,140.25,63.75,140.25,140.25v12.75h38.25c43.35,0,76.5,33.15,76.5,76.5 | ||||
| 			c0,28.05-15.3,53.55-40.8,66.3l38.25,38.25C591.6,438.6,612,400.35,612,357C612,290.7,558.45,234.6,494.7,229.5z M76.5,109.65 | ||||
| 			l71.4,68.85C66.3,183.6,0,249.9,0,331.5c0,84.15,68.85,153,153,153h298.35l51,51l33.15-33.15L109.65,76.5L76.5,109.65z | ||||
| 			 M196.35,229.5l204,204H153c-56.1,0-102-45.9-102-102c0-56.1,45.9-102,102-102H196.35z"/> | ||||
| 	</g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										1
									
								
								peach-web-lite/static/icons/cloud.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | ||||
| <svg height="638pt" viewBox="-20 -129 638.67144 638" width="638pt" xmlns="http://www.w3.org/2000/svg"><path d="m478.90625 132.8125c-4.785156.003906-9.5625.292969-14.3125.863281-12.894531-41.988281-51.628906-70.683593-95.550781-70.773437-10.933594-.011719-21.789063 1.804687-32.121094 5.363281-25.578125-55.308594-86.195313-85.367187-145.699219-72.25-59.511718 13.121094-101.867187 65.875-101.824218 126.808594.003906 10.53125 1.316406 21.019531 3.890624 31.222656-56.695312 8.65625-97.203124 59.46875-92.988281 116.667969 4.207031 57.203125 51.71875 101.542968 109.070313 101.796875h369.535156c66.191406 0 119.847656-53.660157 119.847656-119.851563s-53.65625-119.847656-119.847656-119.847656zm0 219.722656h-369.535156c-49.238282.214844-89.472656-39.253906-90.207032-88.488281-.730468-49.234375 38.304688-89.878906 87.53125-91.132813 3.195313-.089843 6.152344-1.703124 7.957032-4.339843 1.8125-2.640625 2.246094-5.980469 1.171875-8.992188-19.824219-56.730469 9.664062-118.855469 66.152343-139.367187 56.484376-20.511719 118.964844 8.226562 140.15625 64.460937.96875 2.609375 2.976563 4.691407 5.546876 5.753907 2.574218 1.0625 5.46875 1.003906 7.992187-.160157 10.457031-4.863281 21.84375-7.382812 33.371094-7.394531 38 .070312 70.722656 26.835938 78.3125 64.070312 1.085937 5.414063 6.359375 8.914063 11.765625 7.820313 6.511718-1.304687 13.136718-1.96875 19.785156-1.976563 55.160156 0 99.875 44.71875 99.875 99.871094 0 55.160156-44.714844 99.875-99.875 99.875zm0 0"/></svg> | ||||
| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										7
									
								
								peach-web-lite/static/icons/cog.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generated by IcoMoon.io --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20"> | ||||
| <path fill="#000000" d="M7.631 19.702c-0.041 0-0.083-0.005-0.125-0.016-0.898-0.231-1.761-0.587-2.564-1.059-0.233-0.137-0.315-0.434-0.186-0.671 0.159-0.292 0.243-0.622 0.243-0.957 0-1.103-0.897-2-2-2-0.334 0-0.665 0.084-0.957 0.243-0.237 0.129-0.534 0.047-0.671-0.186-0.472-0.804-0.828-1.666-1.059-2.564-0.065-0.254 0.077-0.515 0.325-0.598 0.814-0.274 1.362-1.036 1.362-1.895s-0.547-1.621-1.362-1.895c-0.248-0.084-0.39-0.344-0.325-0.598 0.231-0.898 0.587-1.761 1.059-2.564 0.137-0.233 0.434-0.315 0.671-0.186 0.291 0.159 0.622 0.243 0.957 0.243 1.103 0 2-0.897 2-2 0-0.334-0.084-0.665-0.243-0.957-0.129-0.237-0.047-0.534 0.186-0.671 0.804-0.472 1.666-0.828 2.564-1.059 0.254-0.065 0.515 0.077 0.598 0.325 0.274 0.814 1.036 1.362 1.895 1.362s1.621-0.547 1.895-1.362c0.084-0.248 0.345-0.39 0.598-0.325 0.898 0.231 1.761 0.587 2.564 1.059 0.233 0.137 0.315 0.434 0.186 0.671-0.159 0.292-0.243 0.622-0.243 0.957 0 1.103 0.897 2 2 2 0.334 0 0.665-0.084 0.957-0.243 0.237-0.129 0.534-0.047 0.671 0.186 0.472 0.804 0.828 1.666 1.059 2.564 0.065 0.254-0.077 0.515-0.325 0.598-0.814 0.274-1.362 1.036-1.362 1.895s0.547 1.621 1.362 1.895c0.248 0.084 0.39 0.344 0.325 0.598-0.231 0.898-0.587 1.761-1.059 2.564-0.137 0.233-0.434 0.315-0.671 0.186-0.292-0.159-0.622-0.243-0.957-0.243-1.103 0-2 0.897-2 2 0 0.334 0.084 0.665 0.243 0.957 0.129 0.237 0.047 0.534-0.186 0.671-0.804 0.472-1.666 0.828-2.564 1.059-0.254 0.065-0.515-0.077-0.598-0.325-0.274-0.814-1.036-1.362-1.895-1.362s-1.621 0.547-1.895 1.362c-0.070 0.207-0.264 0.341-0.474 0.341zM10 17c1.127 0 2.142 0.628 2.655 1.602 0.52-0.161 1.026-0.369 1.51-0.622-0.108-0.314-0.164-0.646-0.164-0.98 0-1.654 1.346-3 3-3 0.334 0 0.666 0.056 0.98 0.164 0.253-0.484 0.462-0.989 0.622-1.51-0.974-0.512-1.602-1.527-1.602-2.655s0.628-2.142 1.602-2.655c-0.161-0.52-0.369-1.026-0.622-1.51-0.314 0.108-0.646 0.164-0.98 0.164-1.654 0-3-1.346-3-3 0-0.334 0.056-0.666 0.164-0.98-0.484-0.253-0.989-0.462-1.51-0.622-0.512 0.974-1.527 1.602-2.655 1.602s-2.142-0.628-2.655-1.602c-0.52 0.16-1.026 0.369-1.51 0.622 0.108 0.314 0.164 0.646 0.164 0.98 0 1.654-1.346 3-3 3-0.334 0-0.666-0.056-0.98-0.164-0.253 0.484-0.462 0.989-0.622 1.51 0.974 0.512 1.602 1.527 1.602 2.655s-0.628 2.142-1.602 2.655c0.16 0.52 0.369 1.026 0.622 1.51 0.314-0.108 0.646-0.164 0.98-0.164 1.654 0 3 1.346 3 3 0 0.334-0.056 0.666-0.164 0.98 0.484 0.253 0.989 0.462 1.51 0.622 0.512-0.974 1.527-1.602 2.655-1.602z"></path> | ||||
| <path fill="#000000" d="M10 13c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3zM10 8c-1.103 0-2 0.897-2 2s0.897 2 2 2c1.103 0 2-0.897 2-2s-0.897-2-2-2z"></path> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.8 KiB | 
							
								
								
									
										5
									
								
								peach-web-lite/static/icons/devices.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | ||||
| <?xml version='1.0' encoding='iso-8859-1'?> | ||||
| <!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 457.68 457.68" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 457.68 457.68"> | ||||
|   <path d="m439.48,167.086v-111.249c0-17.81-14.49-32.3-32.3-32.3h-374.88c-17.811,0-32.3,14.49-32.3,32.3v226.63c0,17.81 14.49,32.3 32.3,32.3h106.243l-12.162,13.09h-18.221c-4.142,0-7.5,3.358-7.5,7.5s3.358,7.5 7.5,7.5h104.361v72.334c0,10.449 8.501,18.951 18.951,18.951h80.627c10.449,0 18.951-8.501 18.951-18.951v-15.234h100.94c14.166,0 25.69-11.529 25.69-25.7v-182.59c0-11.563-7.674-21.364-18.2-24.581zm3.2,24.581v2.049h-172.49v-2.049c0-5.9 4.8-10.7 10.7-10.7h151.1c5.895,0.001 10.69,4.801 10.69,10.7zm-130.581,63.364h-41.909v-46.315h172.49v148.491h-111.63v-83.226c0-10.449-8.502-18.95-18.951-18.95zm3.951,28.809h-88.528v-9.858c0-2.178 1.772-3.951 3.951-3.951h80.627c2.178,0 3.951,1.772 3.951,3.951v9.858zm108.429-220.503v102.63h-143.59c-14.171,0-25.7,11.529-25.7,25.7v63.364h-23.718c-10.441,0-18.936,8.488-18.949,18.926h-197.523v-210.62h409.48zm-196.959,235.503h88.528v91.495h-88.528v-91.495zm-195.221-260.303h374.88c6.85,2.13163e-14 12.765,4.012 15.565,9.8h-406.011c2.801-5.788 8.716-9.8 15.566-9.8zm-16.025,250.421h196.247v10.81h-180.222c-7.243-0.001-13.452-4.48-16.025-10.81zm130.582,38.899l12.162-13.09h53.503v13.09h-65.665zm165.242,91.286h-80.627c-2.178,0-3.951-1.772-3.951-3.951v-9.857h88.528v9.857c0.001,2.178-1.772,3.951-3.95,3.951zm119.891-34.185h-100.94v-12.75h111.63v2.05c0,5.899-4.795,10.7-10.69,10.7z"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								peach-web-lite/static/icons/dns.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										46
									
								
								peach-web-lite/static/icons/down-arrow.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,46 @@ | ||||
| <?xml version="1.0" encoding="iso-8859-1"?> | ||||
| <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> | ||||
| <g> | ||||
| 	<g> | ||||
| 		<path d="M441.156,322.876l-48.666-47.386c-3.319-3.243-8.619-3.234-11.93,0.017l-81.894,80.299V8.533 | ||||
| 			c0-4.71-3.823-8.533-8.533-8.533h-68.267c-4.71,0-8.533,3.823-8.533,8.533v347.273l-81.894-80.299 | ||||
| 			c-3.311-3.243-8.602-3.251-11.921-0.017l-48.666,47.386c-1.655,1.604-2.586,3.806-2.586,6.11c0,2.304,0.939,4.506,2.586,6.11 | ||||
| 			l179.2,174.481c1.655,1.613,3.806,2.423,5.948,2.423c2.15,0,4.292-0.811,5.956-2.423l179.2-174.481 | ||||
| 			c1.647-1.604,2.577-3.806,2.577-6.11C443.733,326.682,442.803,324.48,441.156,322.876z M255.991,491.563L89.028,328.986 | ||||
| 			l36.412-35.456l90.445,88.695c2.449,2.406,6.11,3.115,9.276,1.775c3.174-1.331,5.231-4.429,5.231-7.868V17.067h51.2v359.066 | ||||
| 			c0,3.439,2.065,6.537,5.231,7.868c3.166,1.34,6.818,0.631,9.276-1.775l90.445-88.695l36.42,35.456L255.991,491.563z"/> | ||||
| 	</g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| <g> | ||||
| </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										1
									
								
								peach-web-lite/static/icons/enter.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | ||||
| <svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m218.667969 240h-202.667969c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h202.667969c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/><path d="m138.667969 320c-4.097657 0-8.191407-1.558594-11.308594-4.691406-6.25-6.253906-6.25-16.386719 0-22.636719l68.695313-68.691406-68.695313-68.671875c-6.25-6.253906-6.25-16.386719 0-22.636719s16.382813-6.25 22.636719 0l80 80c6.25 6.25 6.25 16.382813 0 22.636719l-80 80c-3.136719 3.132812-7.234375 4.691406-11.328125 4.691406zm0 0"/><path d="m341.332031 512c-23.53125 0-42.664062-19.136719-42.664062-42.667969v-384c0-18.238281 11.605469-34.515625 28.882812-40.511719l128.171875-42.730468c28.671875-8.789063 56.277344 12.480468 56.277344 40.578125v384c0 18.21875-11.605469 34.472656-28.863281 40.488281l-128.214844 42.753906c-4.671875 1.449219-9 2.089844-13.589844 2.089844zm128-480c-1.386719 0-2.558593.171875-3.816406.554688l-127.636719 42.558593c-4.183594 1.453125-7.210937 5.675781-7.210937 10.21875v384c0 7.277344 7.890625 12.183594 14.484375 10.113281l127.636718-42.558593c4.160157-1.453125 7.210938-5.675781 7.210938-10.21875v-384c0-5.867188-4.777344-10.667969-10.667969-10.667969zm0 0"/><path d="m186.667969 106.667969c-8.832031 0-16-7.167969-16-16v-32c0-32.363281 26.300781-58.667969 58.664062-58.667969h240c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16h-240c-14.699219 0-26.664062 11.96875-26.664062 26.667969v32c0 8.832031-7.167969 16-16 16zm0 0"/><path d="m314.667969 448h-85.335938c-32.363281 0-58.664062-26.304688-58.664062-58.667969v-32c0-8.832031 7.167969-16 16-16s16 7.167969 16 16v32c0 14.699219 11.964843 26.667969 26.664062 26.667969h85.335938c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/></svg> | ||||
| After Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										6
									
								
								peach-web-lite/static/icons/envelope.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generated by IcoMoon.io --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20"> | ||||
| <path fill="#000000" d="M17.5 6h-16c-0.827 0-1.5 0.673-1.5 1.5v9c0 0.827 0.673 1.5 1.5 1.5h16c0.827 0 1.5-0.673 1.5-1.5v-9c0-0.827-0.673-1.5-1.5-1.5zM17.5 7c0.030 0 0.058 0.003 0.087 0.008l-7.532 5.021c-0.29 0.193-0.819 0.193-1.109 0l-7.532-5.021c0.028-0.005 0.057-0.008 0.087-0.008h16zM17.5 17h-16c-0.276 0-0.5-0.224-0.5-0.5v-8.566l7.391 4.927c0.311 0.207 0.71 0.311 1.109 0.311s0.798-0.104 1.109-0.311l7.391-4.927v8.566c0 0.276-0.224 0.5-0.5 0.5z"></path> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 777 B | 
							
								
								
									
										7
									
								
								peach-web-lite/static/icons/exit.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,7 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generated by IcoMoon.io --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20"> | ||||
| <path fill="#000000" d="M11.5 8c0.276 0 0.5-0.224 0.5-0.5v-4c0-0.827-0.673-1.5-1.5-1.5h-9c-0.827 0-1.5 0.673-1.5 1.5v12c0 0.746 0.537 1.56 1.222 1.853l5.162 2.212c0.178 0.076 0.359 0.114 0.532 0.114 0.213-0 0.416-0.058 0.589-0.172 0.314-0.207 0.495-0.575 0.495-1.008v-1.5h2.5c0.827 0 1.5-0.673 1.5-1.5v-4c0-0.276-0.224-0.5-0.5-0.5s-0.5 0.224-0.5 0.5v4c0 0.276-0.224 0.5-0.5 0.5h-2.5v-9.5c0-0.746-0.537-1.56-1.222-1.853l-3.842-1.647h7.564c0.276 0 0.5 0.224 0.5 0.5v4c0 0.276 0.224 0.5 0.5 0.5zM6.384 5.566c0.322 0.138 0.616 0.584 0.616 0.934v12c0 0.104-0.028 0.162-0.045 0.173s-0.081 0.014-0.177-0.027l-5.162-2.212c-0.322-0.138-0.616-0.583-0.616-0.934v-12c0-0.079 0.018-0.153 0.051-0.22l5.333 2.286z"></path> | ||||
| <path fill="#000000" d="M18.354 9.146l-3-3c-0.195-0.195-0.512-0.195-0.707 0s-0.195 0.512 0 0.707l2.146 2.146h-6.293c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6.293l-2.146 2.146c-0.195 0.195-0.195 0.512 0 0.707 0.098 0.098 0.226 0.146 0.354 0.146s0.256-0.049 0.354-0.146l3-3c0.195-0.195 0.195-0.512 0-0.707z"></path> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										8
									
								
								peach-web-lite/static/icons/heart-pulse.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generated by IcoMoon.io --> | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20"> | ||||
| <path fill="#000000" d="M9.5 19c-0.084 0-0.167-0.021-0.243-0.063-0.116-0.065-2.877-1.611-5.369-4.082-0.196-0.194-0.197-0.511-0.003-0.707s0.511-0.197 0.707-0.003c1.979 1.962 4.186 3.346 4.908 3.776 0.723-0.431 2.932-1.817 4.908-3.776 0.196-0.194 0.513-0.193 0.707 0.003s0.193 0.513-0.003 0.707c-2.493 2.471-5.253 4.017-5.369 4.082-0.076 0.042-0.159 0.063-0.243 0.063z"></path> | ||||
| <path fill="#000000" d="M1.279 11c-0.188 0-0.368-0.106-0.453-0.287-0.548-1.165-0.826-2.33-0.826-3.463 0-2.895 2.355-5.25 5.25-5.25 0.98 0 2.021 0.367 2.931 1.034 0.532 0.39 0.985 0.86 1.319 1.359 0.334-0.499 0.787-0.969 1.319-1.359 0.91-0.667 1.951-1.034 2.931-1.034 2.895 0 5.25 2.355 5.25 5.25 0 1.133-0.278 2.298-0.826 3.463-0.118 0.25-0.415 0.357-0.665 0.24s-0.357-0.415-0.24-0.665c0.485-1.031 0.731-2.053 0.731-3.037 0-2.343-1.907-4.25-4.25-4.25-1.703 0-3.357 1.401-3.776 2.658-0.068 0.204-0.259 0.342-0.474 0.342s-0.406-0.138-0.474-0.342c-0.419-1.257-2.073-2.658-3.776-2.658-2.343 0-4.25 1.907-4.25 4.25 0 0.984 0.246 2.006 0.731 3.037 0.118 0.25 0.010 0.548-0.24 0.665-0.069 0.032-0.141 0.048-0.212 0.048z"></path> | ||||
| <path fill="#000000" d="M10.515 15c-0.005 0-0.009-0-0.013-0-0.202-0.004-0.569-0.109-0.753-0.766l-1.217-4.334-0.807 3.279c-0.158 0.643-0.525 0.778-0.73 0.8s-0.592-0.027-0.889-0.62l-0.606-1.211c-0.029-0.058-0.056-0.094-0.076-0.117-0.003 0.004-0.007 0.009-0.011 0.015-0.37 0.543-1.192 0.953-1.913 0.953h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.421 0 0.921-0.272 1.087-0.516 0.223-0.327 0.547-0.501 0.891-0.478 0.374 0.025 0.708 0.279 0.917 0.696l0.445 0.89 0.936-3.803c0.158-0.64 0.482-0.779 0.726-0.783s0.572 0.125 0.751 0.76l1.284 4.576 1.178-3.608c0.205-0.628 0.582-0.736 0.788-0.745s0.59 0.068 0.847 0.677l0.724 1.719c0.136 0.322 0.578 0.616 0.927 0.616h1.5c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5h-1.5c-0.747 0-1.559-0.539-1.849-1.228l-0.592-1.406-1.274 3.9c-0.207 0.634-0.566 0.733-0.771 0.733z"></path> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								peach-web-lite/static/icons/hermies.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.4 KiB | 
							
								
								
									
										57
									
								
								peach-web-lite/static/icons/hermies.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB |