diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml index 97c4a92..74631dc 100644 --- a/peach-web/Cargo.toml +++ b/peach-web/Cargo.toml @@ -35,9 +35,11 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" } maintenance = { status = "actively-developed" } [dependencies] +async-std = "1.10" base64 = "0.13.0" dirs = "4.0.0" env_logger = "0.8" +futures = "0.3" golgi = { path = "/home/glyph/Projects/playground/rust/golgi" } lazy_static = "1.4.0" log = "0.4" diff --git a/peach-web/rouille_refactor b/peach-web/rouille_refactor index 8d71a21..debee1a 100644 --- a/peach-web/rouille_refactor +++ b/peach-web/rouille_refactor @@ -14,6 +14,8 @@ we do not need to be super fast or feature-rich. - write the settings route(s) x menu + x guide + - status x scuttlebutt menu - configure_sbot x template @@ -21,9 +23,26 @@ we do not need to be super fast or feature-rich. - might need some thought...render elements or input data - admin x menu - - configure - - change - - reset + x configure + x add + x delete + - auth + x change password + x form + x post + x reset password + x form + x post + x login + x form + x post + x logout + x get + +[ flash messages ] + + - for now, use simple redirects in the handlers + - then add flash messages later - write getter, setter and unsetter for flash messages - from rocket docs diff --git a/peach-web/src/config.rs b/peach-web/src/config.rs index 7fd85ad..5ea7b93 100644 --- a/peach-web/src/config.rs +++ b/peach-web/src/config.rs @@ -38,7 +38,7 @@ impl Config { for l in io::BufReader::new(file).lines() { let line = l?; - if line.len() == 0 { + if line.is_empty() { continue; } if let Some(i) = line.find('=') { diff --git a/peach-web/src/main.rs b/peach-web/src/main.rs index e1ba8dd..fa71ecf 100644 --- a/peach-web/src/main.rs +++ b/peach-web/src/main.rs @@ -118,34 +118,78 @@ fn main() { // static file server // matches on assets in the `static` directory - let response = rouille::match_assets(&request, "static"); + let response = rouille::match_assets(request, "static"); if response.is_success() { return response; } router!(request, (GET) (/) => { - Response::html(routes::home::build()) + Response::html(routes::home::build_template()) + }, + + (GET) (/auth/change) => { + Response::html(routes::authentication::change::build_template()) + }, + + (POST) (/auth/change) => { + routes::authentication::change::handle_form(request) + }, + + (GET) (/auth/login) => { + Response::html(routes::authentication::login::build_template()) + }, + + (POST) (/auth/login) => { + routes::authentication::login::handle_form(request) + }, + + (GET) (/auth/logout) => { + routes::authentication::logout::deauthenticate() + }, + + (GET) (/auth/reset) => { + Response::html(routes::authentication::reset::build_template()) + }, + + (POST) (/auth/reset) => { + routes::authentication::reset::handle_form(request) + }, + + (GET) (/guide) => { + Response::html(routes::guide::build_template()) }, (GET) (/settings) => { - Response::html(routes::settings::menu::build()) + Response::html(routes::settings::menu::build_template()) }, (GET) (/settings/scuttlebutt) => { - Response::html(routes::settings::scuttlebutt::menu::build()) + Response::html(routes::settings::scuttlebutt::menu::build_template()) }, (GET) (/settings/scuttlebutt/configure) => { - Response::html(routes::settings::scuttlebutt::configure::build()) + Response::html(routes::settings::scuttlebutt::configure::build_template()) }, (GET) (/settings/admin) => { - Response::html(routes::settings::admin::menu::build()) + Response::html(routes::settings::admin::menu::build_template()) + }, + + (POST) (/settings/admin/add) => { + routes::settings::admin::add::handle_form(request) }, (GET) (/settings/admin/configure) => { - Response::html(routes::settings::admin::configure::build()) + Response::html(routes::settings::admin::configure::build_template()) + }, + + (POST) (/settings/admin/delete) => { + routes::settings::admin::delete::handle_form(request) + }, + + (GET) (/status/scuttlebutt) => { + Response::html(routes::status::scuttlebutt::build_template()) }, // The code block is called if none of the other blocks matches the request. diff --git a/peach-web/src/routes/authentication.rs_old b/peach-web/src/routes/authentication.rs_old new file mode 100644 index 0000000..3ff8f2b --- /dev/null +++ b/peach-web/src/routes/authentication.rs_old @@ -0,0 +1,333 @@ +use log::info; +use rocket::{ + form::{Form, FromForm}, + get, + http::{Cookie, CookieJar, Status}, + post, + request::{self, FlashMessage, FromRequest, Request}, + response::{Flash, Redirect}, +}; +use rocket_dyn_templates::{tera::Context, Template}; + +use peach_lib::{error::PeachError, password_utils}; + +use crate::error::PeachWebError; +use crate::utils; +use crate::utils::TemplateOrRedirect; +use crate::RocketConfig; + +// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES + +pub const AUTH_COOKIE_KEY: &str = "peachweb_auth"; +pub const ADMIN_USERNAME: &str = "admin"; + +/// Note: Currently we use an empty struct for the Authenticated request guard +/// because there is only one user to be authenticated, and no data needs to be stored here. +/// In a multi-user authentication scheme, we would store the user_id in this struct, +/// and retrieve the correct user via the user_id stored in the cookie. +pub struct Authenticated; + +#[derive(Debug)] +pub enum LoginError { + UserNotLoggedIn, +} + +/// Request guard which returns an empty Authenticated struct from the request +/// if and only if the user has a cookie which proves they are authenticated with peach-web. +/// +/// Note that cookies.get_private uses encryption, which means that this private cookie +/// cannot be inspected, tampered with, or manufactured by clients. +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Authenticated { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + // retrieve auth state from managed state (returns `Option`). + // this value is read from the Rocket.toml config file on start-up + let authentication_is_disabled: bool = *req + .rocket() + .state::() + .map(|config| (&config.disable_auth)) + .unwrap_or(&false); + + if authentication_is_disabled { + let auth = Authenticated {}; + request::Outcome::Success(auth) + } else { + let authenticated = req + .cookies() + .get_private(AUTH_COOKIE_KEY) + .and_then(|cookie| cookie.value().parse().ok()) + .map(|_value: String| Authenticated {}); + match authenticated { + Some(auth) => request::Outcome::Success(auth), + None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)), + } + } + } +} + +// HELPERS AND ROUTES FOR /login + +#[get("/login")] +pub fn login(flash: Option) -> Template { + // retrieve current ui theme + let theme = utils::get_theme(); + + let mut context = Context::new(); + context.insert("theme", &theme); + context.insert("back", &Some("/".to_string())); + context.insert("title", &Some("Login".to_string())); + + // check to see if there is a flash message to display + if let Some(flash) = flash { + context.insert("flash_name", &Some(flash.kind().to_string())); + context.insert("flash_msg", &Some(flash.message().to_string())); + }; + + Template::render("login", &context.into_json()) +} + +#[derive(Debug, FromForm)] +pub struct LoginForm { + pub password: String, +} + +/// Takes in a LoginForm and returns Ok(()) if the password is correct. +/// +/// Note: there is currently only one user, therefore we don't need a username. +pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> { + password_utils::verify_password(&login_form.password) +} + +#[post("/login", data = "")] +pub fn login_post(login_form: Form, cookies: &CookieJar<'_>) -> TemplateOrRedirect { + match verify_login_form(login_form.into_inner()) { + Ok(_) => { + // if successful login, add a cookie indicating the user is authenticated + // and redirect to home page + // NOTE: since we currently have just one user, the value of the cookie + // is just admin (this is arbitrary). + // If we had multiple users, we could put the user_id here. + cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME)); + + TemplateOrRedirect::Redirect(Redirect::to("/")) + } + Err(e) => { + let err_msg = format!("Invalid password: {}", e); + // if unsuccessful login, render /login page again + let mut context = Context::new(); + context.insert("back", &Some("/".to_string())); + context.insert("title", &Some("Login".to_string())); + context.insert("flash_name", &("error".to_string())); + context.insert("flash_msg", &(err_msg)); + + TemplateOrRedirect::Template(Template::render("login", &context.into_json())) + } + } +} + +// HELPERS AND ROUTES FOR /logout + +#[get("/logout")] +pub fn logout(cookies: &CookieJar<'_>) -> Flash { + // logout authenticated user + info!("Attempting deauthentication of user."); + cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY)); + Flash::success(Redirect::to("/login"), "Logged out") +} + +// HELPERS AND ROUTES FOR /reset_password + +#[derive(Debug, FromForm)] +pub struct ResetPasswordForm { + pub temporary_password: String, + pub new_password1: String, + pub new_password2: String, +} + +/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password. +pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> { + info!( + "reset password!: {} {} {}", + password_form.temporary_password, password_form.new_password1, password_form.new_password2 + ); + password_utils::verify_temporary_password(&password_form.temporary_password)?; + // if the previous line did not throw an error, then the secret_link is correct + password_utils::validate_new_passwords( + &password_form.new_password1, + &password_form.new_password2, + )?; + // if the previous line did not throw an error, then the new password is valid + password_utils::set_new_password(&password_form.new_password1)?; + Ok(()) +} + +/// Password reset request handler. This route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password. +#[get("/reset_password")] +pub fn reset_password(flash: Option) -> Template { + // retrieve current ui theme + let theme = utils::get_theme(); + + let mut context = Context::new(); + context.insert("theme", &theme); + context.insert("back", &Some("/".to_string())); + context.insert("title", &Some("Reset Password".to_string())); + + // check to see if there is a flash message to display + if let Some(flash) = flash { + context.insert("flash_name", &Some(flash.kind().to_string())); + context.insert("flash_msg", &Some(flash.message().to_string())); + }; + + Template::render("settings/admin/reset_password", &context.into_json()) +} + +/// Password reset form request handler. This route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password. +#[post("/reset_password", data = "")] +pub fn reset_password_post(reset_password_form: Form) -> Template { + let mut context = Context::new(); + context.insert("back", &Some("/".to_string())); + context.insert("title", &Some("Reset Password".to_string())); + + let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) { + Ok(_) => ( + "success".to_string(), + "New password has been saved. Return home to login".to_string(), + ), + Err(err) => ( + "error".to_string(), + format!("Failed to reset password: {}", err), + ), + }; + + context.insert("flash_name", &Some(flash_name)); + context.insert("flash_msg", &Some(flash_msg)); + + Template::render("settings/admin/reset_password", &context.into_json()) +} + +// HELPERS AND ROUTES FOR /send_password_reset + +/// Page for users who have forgotten their password. +/// This route is used by a user who is not logged in +/// to initiate the sending of a new password reset. +#[get("/forgot_password")] +pub fn forgot_password_page(flash: Option) -> Template { + // retrieve current ui theme + let theme = utils::get_theme(); + + let mut context = Context::new(); + context.insert("theme", &theme); + context.insert("back", &Some("/".to_string())); + context.insert("title", &Some("Send Password Reset".to_string())); + + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.insert("flash_name", &Some(flash.kind().to_string())); + context.insert("flash_msg", &Some(flash.message().to_string())); + }; + + Template::render("settings/admin/forgot_password", &context.into_json()) +} + +/// Send password reset request handler. This route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password. A successful request results +/// in a Scuttlebutt private message being sent to the account of the device admin. +#[post("/send_password_reset")] +pub fn send_password_reset_post() -> Template { + info!("++ send password reset post"); + let mut context = Context::new(); + context.insert("back", &Some("/".to_string())); + context.insert("title", &Some("Send Password Reset".to_string())); + + let (flash_name, flash_msg) = match password_utils::send_password_reset() { + Ok(_) => ( + "success".to_string(), + "A password reset link has been sent to the admin of this device".to_string(), + ), + Err(err) => ( + "error".to_string(), + format!("Failed to send password reset link: {}", err), + ), + }; + + context.insert("flash_name", &Some(flash_name)); + context.insert("flash_msg", &Some(flash_msg)); + + Template::render("settings/admin/forgot_password", &context.into_json()) +} + +// HELPERS AND ROUTES FOR /settings/change_password + +#[derive(Debug, FromForm)] +pub struct PasswordForm { + pub current_password: String, + pub new_password1: String, + pub new_password2: String, +} + +/// 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(password_form: PasswordForm) -> Result<(), PeachWebError> { + info!( + "change password!: {} {} {}", + password_form.current_password, password_form.new_password1, password_form.new_password2 + ); + password_utils::verify_password(&password_form.current_password)?; + // if the previous line did not throw an error, then the old password is correct + password_utils::validate_new_passwords( + &password_form.new_password1, + &password_form.new_password2, + )?; + // if the previous line did not throw an error, then the new password is valid + password_utils::set_new_password(&password_form.new_password1)?; + Ok(()) +} + +/// Change password request handler. This is used by a user who is already logged in. +#[get("/change_password")] +pub fn change_password(flash: Option, _auth: Authenticated) -> Template { + // retrieve current ui theme + let theme = utils::get_theme(); + + let mut context = Context::new(); + context.insert("theme", &theme); + context.insert("back", &Some("/settings/admin".to_string())); + context.insert("title", &Some("Change Password".to_string())); + + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.insert("flash_name", &Some(flash.kind().to_string())); + context.insert("flash_msg", &Some(flash.message().to_string())); + }; + + Template::render("settings/admin/change_password", &context.into_json()) +} + +/// Change password form request handler. This route is used by a user who is already logged in. +#[post("/change_password", data = "")] +pub fn change_password_post(password_form: Form, _auth: Authenticated) -> Template { + let mut context = Context::new(); + context.insert("back", &Some("/settings/admin".to_string())); + context.insert("title", &Some("Change Password".to_string())); + + let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) { + Ok(_) => ( + "success".to_string(), + "New password has been saved".to_string(), + ), + Err(err) => ( + "error".to_string(), + format!("Failed to save new password: {}", err), + ), + }; + + context.insert("flash_name", &Some(flash_name)); + context.insert("flash_msg", &Some(flash_msg)); + + Template::render("settings/admin/change_password", &context.into_json()) +} diff --git a/peach-web/src/routes/authentication/change.rs b/peach-web/src/routes/authentication/change.rs new file mode 100644 index 0000000..2fdd172 --- /dev/null +++ b/peach-web/src/routes/authentication/change.rs @@ -0,0 +1,101 @@ +use log::info; +use maud::{html, PreEscaped}; +use peach_lib::password_utils; +use rouille::{post_input, try_or_400, Request, Response}; + +use crate::{error::PeachWebError, templates}; + +// HELPER AND ROUTES FOR /auth/change (GET and POST) + +/// Password change form template builder. +pub fn build_template() -> PreEscaped { + let form_template = html! { + (PreEscaped("")) + div class="card center" { + form id="changePassword" class="center" action="/auth/change" method="post" { + div style="display: flex; flex-direction: column; margin-bottom: 1rem;" { + (PreEscaped("")) + label for="currentPassword" class="center label-small font-gray" style="width: 80%;" { "CURRENT PASSWORD" } + input id="currentPassword" class="center input" name="current_password" type="password" title="Current password" autofocus; + (PreEscaped("")) + label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" } + input id="newPassword" class="center input" name="new_password1" type="password" title="New password"; + (PreEscaped("")) + label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" } + input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate"; + (PreEscaped("")) + 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" } + } + } + (PreEscaped("")) + // TODO: render flash message + //{% include "snippets/flash_message" %} + } + }; + + // wrap the nav bars around the settings menu template content + // parameters are template, title and back url + let body = + templates::nav::build_template(form_template, "Change Password", Some("/settings/admin")); + + // render the base template with the provided body + templates::base::build_template(body) +} + +/// Verify, validate and set a new password, overwriting the current password. +pub fn save_password( + current_password: &str, + new_password1: &str, + new_password2: &str, +) -> Result<(), PeachWebError> { + info!( + "Attempting password change: {} {} {}", + current_password, new_password1, new_password2 + ); + + // check that the supplied value matches the actual current password + password_utils::verify_password(current_password)?; + + // ensure that both new_password values match + password_utils::validate_new_passwords(new_password1, new_password2)?; + + // hash the password and save the hash to file + password_utils::set_new_password(new_password1)?; + + Ok(()) +} + +/// Parse current and new passwords from the submitted form, save the new +/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect +/// to the change password form URL. +pub fn handle_form(request: &Request) -> Response { + // query the request body for form data + // return a 400 error if the admin_id field is missing + let data = try_or_400!(post_input!(request, { + current_password: String, + new_password1: String, + new_password2: String, + })); + + // save submitted admin id to file + let _result = save_password( + &data.current_password, + &data.new_password1, + &data.new_password2, + ); + + // TODO: match on result and define flash message accordingly + // then send the redirect response + + // redirect to the configure admin page + // TODO: add flash message + Response::redirect_303("/auth/change") +} + +/* + match result { + Ok(_) => Flash::success(Redirect::to(url), "Added SSB administrator"), + Err(e) => Flash::error(Redirect::to(url), format!("Failed to add new admin: {}", e)), + } +*/ diff --git a/peach-web/src/routes/authentication/login.rs b/peach-web/src/routes/authentication/login.rs new file mode 100644 index 0000000..7c0912e --- /dev/null +++ b/peach-web/src/routes/authentication/login.rs @@ -0,0 +1,77 @@ +use log::info; +use maud::{html, PreEscaped}; +use peach_lib::password_utils; +use rouille::{post_input, try_or_400, Request, Response}; + +use crate::templates; + +// HELPER AND ROUTES FOR /auth/login (GET and POST) + +/// Login form template builder. +pub fn build_template() -> PreEscaped { + let form_template = html! { + (PreEscaped("")) + div class="card center" { + form id="login_form" class="center" action="/auth/login" method="post" { + div style="display: flex; flex-direction: column; margin-bottom: 1rem;" { + (PreEscaped("")) + label for="password" class="center label-small font-gray" style="width: 80%;" { "PASSWORD" } + input id="password" name="password" class="center input" type="password" title="Password for given username"; + (PreEscaped("")) + input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login"; + div class="center-text" style="margin-top: 1rem;" { + a href="/settings/admin/forgot_password" class="label-small link font-gray" { "Forgot Password?" } + } + } + (PreEscaped("")) + // TODO: render flash message + //{% include "snippets/flash_message" %} + } + } + }; + + // wrap the nav bars around the settings menu template content + // parameters are template, title and back url + let body = templates::nav::build_template(form_template, "Login", Some("/")); + + // render the base template with the provided body + templates::base::build_template(body) +} + +/// Parse and verify the submitted password. If verification succeeds, set the +/// auth session cookie and redirect to the home page. If not, set a flash +/// message and redirect to the login page. +pub fn handle_form(request: &Request) -> Response { + // query the request body for form data + // return a 400 error if the admin_id field is missing + let data = try_or_400!(post_input!(request, { password: String })); + + // TODO: match on result and define flash message accordingly + // then send the redirect response + match password_utils::verify_password(&data.password) { + Ok(_) => { + info!("Successful login attempt"); + // if successful login, add a cookie indicating the user is authenticated + // and redirect to home page + // NOTE: since we currently have just one user, the value of the cookie + // is just admin (this is arbitrary). + // If we had multiple users, we could put the user_id here. + //cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME)); + + Response::redirect_303("/") + } + Err(_e) => { + info!("Unsuccessful login attempt"); + //let err_msg = format!("Invalid password: {}", e); + // if unsuccessful login, render /login page again + + /* + // TODO: add flash message + context.insert("flash_name", &("error".to_string())); + context.insert("flash_msg", &(err_msg)); + */ + + Response::redirect_303("/auth/login") + } + } +} diff --git a/peach-web/src/routes/authentication/logout.rs b/peach-web/src/routes/authentication/logout.rs new file mode 100644 index 0000000..d3bb81b --- /dev/null +++ b/peach-web/src/routes/authentication/logout.rs @@ -0,0 +1,18 @@ +use log::info; +use rouille::Response; + +// HELPER AND ROUTES FOR /auth/logout (GET) + +/// Deauthenticate the logged-in user by removing the auth cookie. +/// Redirect to the login page. +pub fn deauthenticate() -> Response { + // logout authenticated user + info!("Attempting deauthentication of user."); + // TODO: remove auth cookie + //cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY)); + + // redirect to the login page + // TODO: add flash message + //Flash::success(Redirect::to("/login"), "Logged out") + Response::redirect_303("/auth/login".to_string()) +} diff --git a/peach-web/src/routes/authentication/mod.rs b/peach-web/src/routes/authentication/mod.rs new file mode 100644 index 0000000..f0b9e67 --- /dev/null +++ b/peach-web/src/routes/authentication/mod.rs @@ -0,0 +1,4 @@ +pub mod change; +pub mod login; +pub mod logout; +pub mod reset; diff --git a/peach-web/src/routes/authentication/reset.rs b/peach-web/src/routes/authentication/reset.rs new file mode 100644 index 0000000..37a6ec5 --- /dev/null +++ b/peach-web/src/routes/authentication/reset.rs @@ -0,0 +1,101 @@ +use log::info; +use maud::{html, PreEscaped}; +use peach_lib::password_utils; +use rouille::{post_input, try_or_400, Request, Response}; + +use crate::{error::PeachWebError, templates}; + +// HELPER AND ROUTES FOR /auth/reset (GET and POST) + +/// Password reset form template builder. +pub fn build_template() -> PreEscaped { + let form_template = html! { + (PreEscaped("")) + div class="card center" { + form id="resetPassword" class="center" action="/auth/reset" method="post" { + div style="display: flex; flex-direction: column; margin-bottom: 1rem;" { + (PreEscaped("")) + label for="temporaryPassword" class="center label-small font-gray" style="width: 80%;" { "TEMPORARY PASSWORD" } + input id="temporaryPassword" class="center input" name="temporary_password" type="password" title="Temporary password" autofocus; + (PreEscaped("")) + label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" } + input id="newPassword" class="center input" name="new_password1" type="password" title="New password"; + (PreEscaped("")) + label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" } + input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate"; + (PreEscaped("")) + 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" } + } + } + (PreEscaped("")) + // TODO: render flash message + //{% include "snippets/flash_message" %} + } + }; + + // wrap the nav bars around the settings menu template content + // parameters are template, title and back url + let body = + templates::nav::build_template(form_template, "Reset Password", Some("/settings/admin")); + + // render the base template with the provided body + templates::base::build_template(body) +} + +/// Verify, validate and set a new password, overwriting the current password. +pub fn save_password( + temporary_password: &str, + new_password1: &str, + new_password2: &str, +) -> Result<(), PeachWebError> { + info!( + "Attempting password reset: {} {} {}", + temporary_password, new_password1, new_password2 + ); + + // check that the supplied value matches the actual temporary password + password_utils::verify_temporary_password(temporary_password)?; + + // ensure that both new_password values match + password_utils::validate_new_passwords(new_password1, new_password2)?; + + // hash the password and save the hash to file + password_utils::set_new_password(new_password1)?; + + Ok(()) +} + +/// Parse temporary and new passwords from the submitted form, save the new +/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect +/// to the reset password form URL. +pub fn handle_form(request: &Request) -> Response { + // query the request body for form data + // return a 400 error if the admin_id field is missing + let data = try_or_400!(post_input!(request, { + temporary_password: String, + new_password1: String, + new_password2: String, + })); + + // save submitted admin id to file + let _result = save_password( + &data.temporary_password, + &data.new_password1, + &data.new_password2, + ); + + // TODO: match on result and define flash message accordingly + // then send the redirect response + + // redirect to the configure admin page + // TODO: add flash message + Response::redirect_303("/auth/reset") +} + +/* + match result { + Ok(_) => Flash::success(Redirect::to(url), "Added SSB administrator"), + Err(e) => Flash::error(Redirect::to(url), format!("Failed to add new admin: {}", e)), + } +*/ diff --git a/peach-web/src/routes/guide.rs b/peach-web/src/routes/guide.rs new file mode 100644 index 0000000..2898d0f --- /dev/null +++ b/peach-web/src/routes/guide.rs @@ -0,0 +1,103 @@ +use maud::{html, PreEscaped}; + +use crate::templates; + +/// Guide template builder. +pub fn build_template() -> PreEscaped { + // render the guide template html + let guide_template = html! { + (PreEscaped("")) + div class="card card-wide center" { + div class="capsule capsule-container border-info" { + (PreEscaped("")) + details { + summary class="card-text link" { "Getting started" } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "The Scuttlebutt server (sbot) will be inactive when you first run PeachCloud. This is to allow configuration parameters to be set before it is activated for the first time. Navigate to the " + strong { + a href="/settings/scuttlebutt/configure" class="link font-gray" { + "Sbot Configuration" + } + } + " page to configure your system. The default configuration will be fine for most usecases." + } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "Once the configuration is set, navigate to the " + strong { + a href="/settings/scuttlebutt" class="link font-gray" { + "Scuttlebutt settings menu" + } + } + " to start the sbot. If the server starts successfully, you will see a green smiley face on the home page. If the face is orange and sleeping, that means the sbot is still inactive (ie. the process is not running). If the face is red and dead, that means the sbot failed to start - indicated an error. For now, the best way to gain insight into the problem is to check the systemd log. Open a terminal and enter: " + code { "systemctl --user status go-sbot.service" } + ". The log output may give some clues about the source of the error." + } + } + (PreEscaped("")) + details { + summary class="card-text link" { "Submit a bug report" } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "Bug reports can be submitted by " + strong { + a href="https://git.coopcloud.tech/PeachCloud/peach-workspace/issues/new?template=BUG_TEMPLATE.md" class="link font-gray" { + "filing an issue" + } + } + " on the peach-workspace git repo. Before filing a report, first check to see if an issue already exists for the bug you've encountered. If not, you're invited to submit a new report; the template will guide you through several questions." + } + } + (PreEscaped("")) + details { + summary class="card-text link" { "Share feedback & request support" } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "You're invited to share your thoughts and experiences of PeachCloud in the #peachcloud channel on Scuttlebutt. The channel is also a good place to ask for help." + } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "Alternatively, we have a " + strong { + a href="https://matrix.to/#/#peachcloud:matrix.org" class="link font-gray" { + "Matrix channel" + } + } + " for discussion about PeachCloud and you can also reach out to @glyph " + strong { + a href="mailto:glyph@mycelial.technology" class="link font-gray" { + "via email" + } + } + "." + } + } + (PreEscaped("")) + details { + summary class="card-text link" { "Contribute to PeachCloud" } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "PeachCloud is free, open-source software and relies on donations and grants to fund develop. Donations can be made on our " + strong { + a href="https://opencollective.com/peachcloud" class="link font-gray" { + "Open Collective" + } + } + " page." + } + p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" { + "Programmers, designers, artists and writers are also welcome to contribute to the project. Please visit the " + strong { + a href="https://git.coopcloud.tech/PeachCloud/peach-workspace" class="link font-gray" { + "main PeachCloud git repository" + } + } + " to find out more details or contact the team via Scuttlebutt, Matrix or email." + } + } + } + } + }; + + // wrap the nav bars around the home template content + // title is "" and back button link is `None` because this is the homepage + let body = templates::nav::build_template(guide_template, "Guide", Some("/")); + + // render the base template with the provided body + templates::base::build_template(body) +} diff --git a/peach-web/src/routes/home.rs b/peach-web/src/routes/home.rs index 2dc1223..76e9b4d 100644 --- a/peach-web/src/routes/home.rs +++ b/peach-web/src/routes/home.rs @@ -41,7 +41,7 @@ fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) { } /// Home template builder. -pub fn build<'a>() -> PreEscaped { +pub fn build_template() -> PreEscaped { let (center_circle_class, center_circle_text, status_circle_class) = render_status_elements(); // render the home template html @@ -111,8 +111,8 @@ pub fn build<'a>() -> PreEscaped { // wrap the nav bars around the home template content // title is "" and back button link is `None` because this is the homepage - let body = templates::nav::build(home_template, "", None); + let body = templates::nav::build_template(home_template, "", None); // render the base template with the provided body - templates::base::build(body) + templates::base::build_template(body) } diff --git a/peach-web/src/routes/mod.rs b/peach-web/src/routes/mod.rs index 1c94673..37655b6 100644 --- a/peach-web/src/routes/mod.rs +++ b/peach-web/src/routes/mod.rs @@ -1,7 +1,8 @@ -//pub mod authentication; +pub mod authentication; //pub mod catchers; //pub mod index; +pub mod guide; pub mod home; //pub mod scuttlebutt; pub mod settings; -//pub mod status; +pub mod status; diff --git a/peach-web/src/routes/settings/admin/add.rs b/peach-web/src/routes/settings/admin/add.rs new file mode 100644 index 0000000..cc63873 --- /dev/null +++ b/peach-web/src/routes/settings/admin/add.rs @@ -0,0 +1,33 @@ +use peach_lib::config_manager; +use rouille::{post_input, try_or_400, Request, Response}; + +// HELPER AND ROUTES FOR /settings/admin/add + +/// Parse an `admin_id` from the submitted form, save it to file +/// (`/var/lib/peachcloud/config.yml`) and redirect to the administrator +/// configuration URL. +pub fn handle_form(request: &Request) -> Response { + // query the request body for form data + // return a 400 error if the admin_id field is missing + let data = try_or_400!(post_input!(request, { + // the public key of a desired administrator + ssb_id: String, + })); + + // save submitted admin id to file + let _result = config_manager::add_ssb_admin_id(&data.ssb_id); + + // TODO: match on result and define flash message accordingly + // then send the redirect response + + // redirect to the configure admin page + // TODO: add flash message + Response::redirect_303("/settings/admin/configure") +} + +/* + match result { + Ok(_) => Flash::success(Redirect::to(url), "Added SSB administrator"), + Err(e) => Flash::error(Redirect::to(url), format!("Failed to add new admin: {}", e)), + } +*/ diff --git a/peach-web/src/routes/settings/admin/configure.rs b/peach-web/src/routes/settings/admin/configure.rs index d0f7329..7edcb9e 100644 --- a/peach-web/src/routes/settings/admin/configure.rs +++ b/peach-web/src/routes/settings/admin/configure.rs @@ -4,7 +4,7 @@ use peach_lib::config_manager; use crate::templates; /// Administrator settings menu template builder. -pub fn build() -> PreEscaped { +pub fn build_template() -> PreEscaped { // attempt to load peachcloud config file let ssb_admins = config_manager::load_peach_config() .ok() @@ -40,7 +40,7 @@ pub fn build() -> PreEscaped { input class="button button-primary center" type="submit" title="Add SSB administrator" value="Add Admin"; (PreEscaped("")) @if ssb_admins.is_none() { - (templates::flash::build("error", "Failed to read PeachCloud configuration file")) + (templates::flash::build_template("error", "Failed to read PeachCloud configuration file")) } } } @@ -48,12 +48,12 @@ pub fn build() -> PreEscaped { // wrap the nav bars around the settings menu template content // parameters are template, title and back url - let body = templates::nav::build( + let body = templates::nav::build_template( menu_template, "Configure Administrators", Some("/settings/admin"), ); // render the base template with the provided body - templates::base::build(body) + templates::base::build_template(body) } diff --git a/peach-web/src/routes/settings/admin/delete.rs b/peach-web/src/routes/settings/admin/delete.rs new file mode 100644 index 0000000..5a21c36 --- /dev/null +++ b/peach-web/src/routes/settings/admin/delete.rs @@ -0,0 +1,36 @@ +use peach_lib::config_manager; +use rouille::{post_input, try_or_400, Request, Response}; + +// HELPERS AND ROUTES FOR /settings/admin/delete + +/// Parse an `admin_id` from the submitted form, delete it from file +/// (`/var/lib/peachcloud/config.yml`) and redirect to the administrator +/// configuration URL. +pub fn handle_form(request: &Request) -> Response { + // query the request body for form data + // return a 400 error if the admin_id field is missing + let data = try_or_400!(post_input!(request, { + // the public key of a desired administrator + ssb_id: String, + })); + + // remove submitted admin id from file + let _result = config_manager::delete_ssb_admin_id(&data.ssb_id); + + // TODO: match on result and define flash message accordingly + // then send the redirect response + + // redirect to the configure admin page + // TODO: add flash message + Response::redirect_303("/settings/admin/configure") +} + +/* + match result { + Ok(_) => Flash::success(Redirect::to(url), "Removed SSB administrator"), + Err(e) => Flash::error( + Redirect::to(url), + format!("Failed to remove admin id: {}", e), + ), + } +*/ diff --git a/peach-web/src/routes/settings/admin/menu.rs b/peach-web/src/routes/settings/admin/menu.rs index 24f57b3..036fea0 100644 --- a/peach-web/src/routes/settings/admin/menu.rs +++ b/peach-web/src/routes/settings/admin/menu.rs @@ -3,15 +3,15 @@ use maud::{html, PreEscaped}; use crate::templates; /// Administrator settings menu template builder. -pub fn build() -> PreEscaped { +pub fn build_template() -> PreEscaped { let menu_template = html! { (PreEscaped("")) div class="card center" { (PreEscaped("")) div id="settingsButtons" { a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin" { "Configure Admin" } - a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password" { "Change Password" } - a id="reset" class="button button-primary center" href="/settings/admin/forgot_password" title="Reset Password" { "Reset Password" } + a id="change" class="button button-primary center" href="/auth/change" title="Change Password" { "Change Password" } + a id="reset" class="button button-primary center" href="/auth/reset" title="Reset Password" { "Reset Password" } } } @@ -19,8 +19,9 @@ pub fn build() -> PreEscaped { // wrap the nav bars around the settings menu template content // parameters are template, title and back url - let body = templates::nav::build(menu_template, "Administrator Settings", Some("/settings")); + let body = + templates::nav::build_template(menu_template, "Administrator Settings", Some("/settings")); // render the base template with the provided body - templates::base::build(body) + templates::base::build_template(body) } diff --git a/peach-web/src/routes/settings/admin/mod.rs b/peach-web/src/routes/settings/admin/mod.rs index 485da58..93bb64c 100644 --- a/peach-web/src/routes/settings/admin/mod.rs +++ b/peach-web/src/routes/settings/admin/mod.rs @@ -1,2 +1,4 @@ +pub mod add; pub mod configure; +pub mod delete; pub mod menu; diff --git a/peach-web/src/routes/settings/menu.rs b/peach-web/src/routes/settings/menu.rs index 121aefd..9a3c16c 100644 --- a/peach-web/src/routes/settings/menu.rs +++ b/peach-web/src/routes/settings/menu.rs @@ -5,7 +5,7 @@ use crate::{templates, CONFIG}; // TODO: flash message implementation for rouille // /// Settings menu template builder. -pub fn build() -> PreEscaped { +pub fn build_template() -> PreEscaped { let menu_template = html! { (PreEscaped("")) div class="card center" { @@ -23,8 +23,8 @@ pub fn build() -> PreEscaped { // wrap the nav bars around the settings menu template content // parameters are template, title and back url - let body = templates::nav::build(menu_template, "Settings", Some("/")); + let body = templates::nav::build_template(menu_template, "Settings", Some("/")); // render the base template with the provided body - templates::base::build(body) + templates::base::build_template(body) } diff --git a/peach-web/src/routes/settings/scuttlebutt/configure.rs b/peach-web/src/routes/settings/scuttlebutt/configure.rs index fdbdc1c..c53c1c8 100644 --- a/peach-web/src/routes/settings/scuttlebutt/configure.rs +++ b/peach-web/src/routes/settings/scuttlebutt/configure.rs @@ -41,7 +41,7 @@ fn read_status_and_config() -> (String, SbotConfig, String, String) { } /// Scuttlebutt settings menu template builder. -pub fn build() -> PreEscaped { +pub fn build_template() -> PreEscaped { let (run_on_startup, sbot_config, ip, port) = read_status_and_config(); let menu_template = html! { @@ -112,7 +112,7 @@ pub fn build() -> PreEscaped { input type="text" id="database_dir" name="repo" value=(sbot_config.repo); } div class="center" { - @if sbot_config.localadv == true { + @if sbot_config.localadv { input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv" checked; } @else { input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv"; @@ -121,7 +121,7 @@ pub fn build() -> PreEscaped { "Enable LAN Broadcasting" } br; - @if sbot_config.localdiscov == true { + @if sbot_config.localdiscov { input type="checkbox" id="lanDiscovery" style="margin-bottom: 1rem;" name="localdiscov" checked; } @else { input type="checkbox" id="lanDiscovery" style="margin-bottom: 1rem;" name="localdiscov"; @@ -135,7 +135,7 @@ pub fn build() -> PreEscaped { } label class="font-normal" for="startup" title="Run the pub automatically on system startup" { "Run pub when computer starts" } br; - @if sbot_config.repair == true { + @if sbot_config.repair { input type="checkbox" id="repair" name="repair" checked; } @else { input type="checkbox" id="repair" name="repair"; @@ -162,8 +162,9 @@ pub fn build() -> PreEscaped { // wrap the nav bars around the settings menu template content // parameters are template, title and back url - let body = templates::nav::build(menu_template, "Scuttlebutt Settings", Some("/settings")); + let body = + templates::nav::build_template(menu_template, "Scuttlebutt Settings", Some("/settings")); // render the base template with the provided body - templates::base::build(body) + templates::base::build_template(body) } diff --git a/peach-web/src/routes/settings/scuttlebutt/menu.rs b/peach-web/src/routes/settings/scuttlebutt/menu.rs index c8b35df..58c9822 100644 --- a/peach-web/src/routes/settings/scuttlebutt/menu.rs +++ b/peach-web/src/routes/settings/scuttlebutt/menu.rs @@ -29,7 +29,7 @@ fn render_process_buttons() -> PreEscaped { } /// Scuttlebutt settings menu template builder. -pub fn build() -> PreEscaped { +pub fn build_template() -> PreEscaped { let menu_template = html! { (PreEscaped("")) div class="card center" { @@ -44,8 +44,9 @@ pub fn build() -> PreEscaped { // wrap the nav bars around the settings menu template content // parameters are template, title and back url - let body = templates::nav::build(menu_template, "Scuttlebutt Settings", Some("/settings")); + let body = + templates::nav::build_template(menu_template, "Scuttlebutt Settings", Some("/settings")); // render the base template with the provided body - templates::base::build(body) + templates::base::build_template(body) } diff --git a/peach-web/src/routes/status/mod.rs b/peach-web/src/routes/status/mod.rs index a2e0f54..90d533e 100644 --- a/peach-web/src/routes/status/mod.rs +++ b/peach-web/src/routes/status/mod.rs @@ -1,3 +1,3 @@ -pub mod device; -pub mod network; +//pub mod device; +//pub mod network; pub mod scuttlebutt; diff --git a/peach-web/src/routes/status/scuttlebutt.rs b/peach-web/src/routes/status/scuttlebutt.rs index 34aa61d..f2eb965 100644 --- a/peach-web/src/routes/status/scuttlebutt.rs +++ b/peach-web/src/routes/status/scuttlebutt.rs @@ -1,37 +1,271 @@ -use rocket::{get, State}; -use rocket_dyn_templates::Template; +use std::error::Error; -use crate::routes::authentication::Authenticated; -use crate::{context::scuttlebutt::StatusContext, RocketConfig}; +use async_std::task; +use futures::stream::TryStreamExt; +use golgi::{messages::SsbMessageValue, Sbot}; +use maud::{html, Markup, PreEscaped}; +use peach_lib::sbot::{SbotConfig, SbotStatus}; -// HELPERS AND ROUTES FOR /status/scuttlebutt +use crate::{error::PeachWebError, templates}; -#[get("/scuttlebutt")] -pub async fn scuttlebutt_status(_auth: Authenticated, config: &State) -> Template { - let context = StatusContext::build().await; +/* +{# ASSIGN VARIABLES #} +{# ---------------- #} +{%- if sbot_status.memory -%} +{% set mem = sbot_status.memory / 1024 / 1024 | round | int -%} +{%- else -%} +{% set mem = "0" -%} +{%- endif -%} +{%- if sbot_status.blobstore -%} +{% set blobs = sbot_status.blobstore / 1024 / 1024 | round | int -%} +{%- else -%} +{% set blobs = "0" -%} +{%- endif -%} +*/ - let back = if config.standalone_mode { - // return to home page - Some("/".to_string()) - } else { - // return to status menu - Some("/status".to_string()) +// HELPER FUNCTIONS + +pub async fn init_sbot_with_config( + sbot_config: &Option, +) -> Result { + // initialise sbot connection with ip:port and shscap from config file + let sbot_client = match sbot_config { + // TODO: panics if we pass `Some(conf.shscap)` as second arg + Some(conf) => { + let ip_port = conf.lis.clone(); + Sbot::init(Some(ip_port), None).await? + } + None => Sbot::init(None, None).await?, }; - match context { - Ok(mut context) => { - // define back arrow url based on mode - context.back = back; + Ok(sbot_client) +} - Template::render("status/scuttlebutt", &context) +fn latest_sequence_number() -> Result> { + // retrieve latest go-sbot configuration parameters + let sbot_config = SbotConfig::read().ok(); + + task::block_on(async { + let mut sbot_client = init_sbot_with_config(&sbot_config).await?; + + // retrieve the local id + let id = sbot_client.whoami().await?; + + let history_stream = sbot_client.create_history_stream(id).await?; + let mut msgs: Vec = history_stream.try_collect().await?; + + // reverse the list of messages so we can easily reference the latest one + msgs.reverse(); + + // return the sequence number of the latest msg + Ok(msgs[0].sequence) + }) +} + +fn downtime_element(downtime: &Option) -> Markup { + match downtime { + Some(time) => { + html! { + label class="label-small font-gray" for="sbotDowntime" title="go-sbot downtime" style="margin-top: 0.5rem;" { "DOWNTIME" } + p id="sbotDowntime" class="card-text" title="Downtime" { (time) } + } } - Err(_) => { - let mut context = StatusContext::default(); + _ => html! { (PreEscaped("")) }, + } +} - // define back arrow url based on mode - context.back = back; +fn uptime_element(uptime: &Option) -> Markup { + match uptime { + Some(time) => { + html! { + label class="label-small font-gray" for="sbotUptime" title="go-sbot uptime" style="margin-top: 0.5rem;" { "UPTIME" } + p id="sbotUptime" class="card-text" title="Uptime" { (time) } + } + } + _ => html! { (PreEscaped("")) }, + } +} - Template::render("status/scuttlebutt", &context) +fn run_on_startup_element(boot_state: &Option) -> Markup { + match boot_state { + Some(state) if state == "enabled" => { + html! { + p id="runOnStartup" class="card-text" title="Enabled" { "Enabled" } + } + } + _ => { + html! { + p id="runOnStartup" class="card-text" title="Disabled" { "Disabled" } + } } } } + +fn database_element(state: &str) -> Markup { + // retrieve the sequence number of the latest message in the sbot database + let sequence_num = latest_sequence_number(); + + if state == "active" && sequence_num.is_ok() { + let number = sequence_num.unwrap(); + html! { + label class="card-text" style="margin-right: 5px;" { (number) } + label class="label-small font-gray" { "MESSAGES IN LOCAL DATABASE" } + } + } else { + html! { label class="label-small font-gray" { "DATABASE UNAVAILABLE" } } + } +} + +/// Read the state of the go-sbot process and define status-related +/// elements accordingly. +fn render_status_elements<'a>() -> (String, &'a str, &'a str, Markup, Markup, Markup) { + // retrieve go-sbot systemd process status + let sbot_status = SbotStatus::read(); + + // conditionally render the following elements: + // state, capsule border class, sbot icon class, uptime or downtime element, + // run on startup element and database (sequence number) element + if let Ok(status) = sbot_status { + match status.state { + Some(state) if state == "active" => ( + "ACTIVE".to_string(), + "capsule capsule-container border-success", + "center icon icon-active", + uptime_element(&status.uptime), + run_on_startup_element(&status.boot_state), + database_element("active"), + ), + Some(state) if state == "inactive" => ( + "INACTIVE".to_string(), + "capsule capsule-container border-warning", + "center icon icon-inactive", + downtime_element(&status.downtime), + run_on_startup_element(&status.boot_state), + database_element("inactive"), + ), + // state is neither active nor inactive (might be failed) + Some(state) => ( + state.to_string(), + "capsule capsule-container border-danger", + "center icon icon-inactive", + downtime_element(&None), + run_on_startup_element(&status.boot_state), + database_element("failed"), + ), + None => ( + "UNAVAILABLE".to_string(), + "capsule capsule-container border-danger", + "center icon icon-inactive", + downtime_element(&None), + run_on_startup_element(&status.boot_state), + database_element("unavailable"), + ), + } + // show an error state if the attempt to read the go-sbot process + // status fails + } else { + ( + "PROCESS QUERY FAILED".to_string(), + "capsule capsule-container border-danger", + "center icon icon-inactive", + downtime_element(&None), + run_on_startup_element(&None), + database_element("error"), + ) + } +} + +/// Scuttlebutt status template builder. +pub fn build_template() -> PreEscaped { + let ( + sbot_state, + capsule_class, + sbot_icon_class, + uptime_downtime_element, + run_on_startup_element, + database_element, + ) = render_status_elements(); + + let status_template = html! { + (PreEscaped("")) + div class="card center" { + (PreEscaped("")) + div class=(capsule_class) { + (PreEscaped("")) + div class="two-grid" title="go-sbot process state" { + (PreEscaped("")) + a class="link two-grid-top-right" href="/settings/scuttlebutt" title="Configure Scuttlebutt settings" { + img id="configureNetworking" class="icon-small icon-active" src="/icons/cog.svg" alt="Configure"; + } + (PreEscaped("")) + (PreEscaped("")) + div class="grid-column-1" { + img id="sbotStateIcon" class=(sbot_icon_class) src="/icons/hermies.svg" alt="Hermies"; + label id="sbotStateLabel" for="sbotStateIcon" class="center label-small font-gray" style="margin-top: 0.5rem;" title="Sbot state" { + (sbot_state) + } + } + (PreEscaped("")) + (PreEscaped("")) + div class="grid-column-2" { + label class="label-small font-gray" for="sbotVersion" title="go-sbot version" { + "VERSION" + } + p id="sbotVersion" class="card-text" title="Version" { + "1.1.0-alpha" + } + (uptime_downtime_element) + label class="label-small font-gray" for="sbotBootState" title="go-sbot boot state" style="margin-top: 0.5rem;" { "RUN ON STARTUP" } + (run_on_startup_element) + } + } + hr style="color: var(--light-gray);"; + div id="middleSection" style="margin-top: 1rem;" { + div id="sbotInfo" class="center" style="display: flex; justify-content: space-between; width: 90%;" { + div class="center" style="display: flex; align-items: last baseline;" { + (database_element) + } + } + } + hr style="color: var(--light-gray);"; + /* + (PreEscaped(" +
+
+ +
+ +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ + + */ + } + } + }; + + // wrap the nav bars around the settings menu template content + // parameters are template, title and back url + let body = templates::nav::build_template(status_template, "Settings", Some("/")); + + // render the base template with the provided body + templates::base::build_template(body) +} diff --git a/peach-web/src/routes/status/scuttlebutt.rs_old b/peach-web/src/routes/status/scuttlebutt.rs_old new file mode 100644 index 0000000..34aa61d --- /dev/null +++ b/peach-web/src/routes/status/scuttlebutt.rs_old @@ -0,0 +1,37 @@ +use rocket::{get, State}; +use rocket_dyn_templates::Template; + +use crate::routes::authentication::Authenticated; +use crate::{context::scuttlebutt::StatusContext, RocketConfig}; + +// HELPERS AND ROUTES FOR /status/scuttlebutt + +#[get("/scuttlebutt")] +pub async fn scuttlebutt_status(_auth: Authenticated, config: &State) -> Template { + let context = StatusContext::build().await; + + let back = if config.standalone_mode { + // return to home page + Some("/".to_string()) + } else { + // return to status menu + Some("/status".to_string()) + }; + + match context { + Ok(mut context) => { + // define back arrow url based on mode + context.back = back; + + Template::render("status/scuttlebutt", &context) + } + Err(_) => { + let mut context = StatusContext::default(); + + // define back arrow url based on mode + context.back = back; + + Template::render("status/scuttlebutt", &context) + } + } +} diff --git a/peach-web/src/templates/base.rs b/peach-web/src/templates/base.rs index 48430af..20019a0 100644 --- a/peach-web/src/templates/base.rs +++ b/peach-web/src/templates/base.rs @@ -3,7 +3,7 @@ use maud::{html, PreEscaped, DOCTYPE}; /// Base template builder. /// /// Takes an HTML body as input and splices it into the base template. -pub fn build(body: PreEscaped) -> PreEscaped { +pub fn build_template(body: PreEscaped) -> PreEscaped { html! { (DOCTYPE) html lang="en" data-theme="light"; diff --git a/peach-web/src/templates/flash.rs b/peach-web/src/templates/flash.rs index 1a4c08e..bc597b2 100644 --- a/peach-web/src/templates/flash.rs +++ b/peach-web/src/templates/flash.rs @@ -3,7 +3,7 @@ use maud::{html, Markup}; /// Flash message template builder. /// /// Render a flash elements based on the given flash name and message. -pub fn build(flash_name: &str, flash_msg: &str) -> Markup { +pub fn build_template(flash_name: &str, flash_msg: &str) -> Markup { let flash_class = match flash_name { "success" => "capsule center-text flash-message font-normal border-success", "info" => "capsule center-text flash-message font-normal border-info", diff --git a/peach-web/src/templates/nav.rs b/peach-web/src/templates/nav.rs index 7c67ff2..221aa6b 100644 --- a/peach-web/src/templates/nav.rs +++ b/peach-web/src/templates/nav.rs @@ -5,7 +5,11 @@ use crate::utils; /// Navigation template builder. /// /// Takes the main HTML content as input and splices it into the navigation template. -pub fn build(main: PreEscaped, title: &str, back: Option<&str>) -> PreEscaped { +pub fn build_template( + main: PreEscaped, + title: &str, + back: Option<&str>, +) -> PreEscaped { // retrieve the current theme value let theme = utils::get_theme(); @@ -38,7 +42,7 @@ pub fn build(main: PreEscaped, title: &str, back: Option<&str>) -> PreEs img class="icon-medium nav-icon-left icon-active" src="/icons/back.svg" alt="Back"; } h1 class="nav-title" { (title) } - a class="nav-item" id="logoutButton" href="/logout" title="Logout" { + a class="nav-item" id="logoutButton" href="/auth/logout" title="Logout" { img class="icon-medium nav-icon-right icon-active" src="/icons/enter.svg" alt="Enter"; } }