add base templates, settings routes and auth routes

This commit is contained in:
glyph 2022-03-17 11:01:36 +02:00
parent 7c98cfcd5d
commit 8455e8089c
27 changed files with 1219 additions and 67 deletions

View File

@ -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"

View File

@ -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

View File

@ -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('=') {

View File

@ -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.

View File

@ -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<Self, Self::Error> {
// retrieve auth state from managed state (returns `Option<bool>`).
// this value is read from the Rocket.toml config file on start-up
let authentication_is_disabled: bool = *req
.rocket()
.state::<RocketConfig>()
.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<FlashMessage>) -> 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 = "<login_form>")]
pub fn login_post(login_form: Form<LoginForm>, 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<Redirect> {
// 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<FlashMessage>) -> 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 = "<reset_password_form>")]
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> 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<FlashMessage>) -> 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<FlashMessage>, _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 = "<password_form>")]
pub fn change_password_post(password_form: Form<PasswordForm>, _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())
}

View File

@ -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<String> {
let form_template = html! {
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
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("<!-- input for current password -->"))
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("<!-- input for new password -->"))
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("<!-- input for duplicate new password -->"))
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("<!-- save (form submission) button -->"))
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("<!-- FLASH MESSAGE -->"))
// 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)),
}
*/

View File

@ -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<String> {
let form_template = html! {
(PreEscaped("<!-- LOGIN FORM -->"))
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("<!-- input for password -->"))
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("<!-- login (form submission) button -->"))
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("<!-- FLASH MESSAGE -->"))
// 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")
}
}
}

View File

@ -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())
}

View File

@ -0,0 +1,4 @@
pub mod change;
pub mod login;
pub mod logout;
pub mod reset;

View File

@ -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<String> {
let form_template = html! {
(PreEscaped("<!-- RESET PASSWORD PAGE -->"))
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("<!-- input for temporary password -->"))
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("<!-- input for new password1 -->"))
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("<!-- input for duplicate new password -->"))
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("<!-- save (form submission) button -->"))
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("<!-- FLASH MESSAGE -->"))
// 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)),
}
*/

View File

@ -0,0 +1,103 @@
use maud::{html, PreEscaped};
use crate::templates;
/// Guide template builder.
pub fn build_template() -> PreEscaped<String> {
// render the guide template html
let guide_template = html! {
(PreEscaped("<!-- GUIDE -->"))
div class="card card-wide center" {
div class="capsule capsule-container border-info" {
(PreEscaped("<!-- GETTING STARTED -->"))
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("<!-- BUG REPORTS -->"))
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("<!-- REQUEST SUPPORT -->"))
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("<!-- CONTRIBUTE -->"))
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)
}

View File

@ -41,7 +41,7 @@ fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) {
}
/// Home template builder.
pub fn build<'a>() -> PreEscaped<String> {
pub fn build_template() -> PreEscaped<String> {
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<String> {
// 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)
}

View File

@ -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;

View File

@ -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)),
}
*/

View File

@ -4,7 +4,7 @@ use peach_lib::config_manager;
use crate::templates;
/// Administrator settings menu template builder.
pub fn build() -> PreEscaped<String> {
pub fn build_template() -> PreEscaped<String> {
// attempt to load peachcloud config file
let ssb_admins = config_manager::load_peach_config()
.ok()
@ -40,7 +40,7 @@ pub fn build() -> PreEscaped<String> {
input class="button button-primary center" type="submit" title="Add SSB administrator" value="Add Admin";
(PreEscaped("<!-- FLASH MESSAGE -->"))
@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<String> {
// 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)
}

View File

@ -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),
),
}
*/

View File

@ -3,15 +3,15 @@ use maud::{html, PreEscaped};
use crate::templates;
/// Administrator settings menu template builder.
pub fn build() -> PreEscaped<String> {
pub fn build_template() -> PreEscaped<String> {
let menu_template = html! {
(PreEscaped("<!-- ADMIN SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
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<String> {
// 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)
}

View File

@ -1,2 +1,4 @@
pub mod add;
pub mod configure;
pub mod delete;
pub mod menu;

View File

@ -5,7 +5,7 @@ use crate::{templates, CONFIG};
// TODO: flash message implementation for rouille
//
/// Settings menu template builder.
pub fn build() -> PreEscaped<String> {
pub fn build_template() -> PreEscaped<String> {
let menu_template = html! {
(PreEscaped("<!-- SETTINGS MENU -->"))
div class="card center" {
@ -23,8 +23,8 @@ pub fn build() -> PreEscaped<String> {
// 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)
}

View File

@ -41,7 +41,7 @@ fn read_status_and_config() -> (String, SbotConfig, String, String) {
}
/// Scuttlebutt settings menu template builder.
pub fn build() -> PreEscaped<String> {
pub fn build_template() -> PreEscaped<String> {
let (run_on_startup, sbot_config, ip, port) = read_status_and_config();
let menu_template = html! {
@ -112,7 +112,7 @@ pub fn build() -> PreEscaped<String> {
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<String> {
"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<String> {
}
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<String> {
// 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)
}

View File

@ -29,7 +29,7 @@ fn render_process_buttons() -> PreEscaped<String> {
}
/// Scuttlebutt settings menu template builder.
pub fn build() -> PreEscaped<String> {
pub fn build_template() -> PreEscaped<String> {
let menu_template = html! {
(PreEscaped("<!-- SCUTTLEBUTT SETTINGS MENU -->"))
div class="card center" {
@ -44,8 +44,9 @@ pub fn build() -> PreEscaped<String> {
// 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)
}

View File

@ -1,3 +1,3 @@
pub mod device;
pub mod network;
//pub mod device;
//pub mod network;
pub mod scuttlebutt;

View File

@ -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<RocketConfig>) -> 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<SbotConfig>,
) -> Result<Sbot, PeachWebError> {
// 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<u64, Box<dyn Error>> {
// 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<SsbMessageValue> = 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<String>) -> 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<String>) -> 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<String>) -> 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<String> {
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("<!-- SCUTTLEBUTT STATUS -->"))
div class="card center" {
(PreEscaped("<!-- SBOT INFO BOX -->"))
div class=(capsule_class) {
(PreEscaped("<!-- SBOT STATUS GRID -->"))
div class="two-grid" title="go-sbot process state" {
(PreEscaped("<!-- top-right config icon -->"))
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("<!-- left column -->"))
(PreEscaped("<!-- go-sbot state icon with label -->"))
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("<!-- right column -->"))
(PreEscaped("<!-- go-sbot version and uptime / downtime with labels -->"))
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("<!-- THREE-ACROSS STACK -->
<div class="three-grid card-container" style="margin-top: 1rem;">
<div class="stack">
<img class="icon icon-active" title="Hops" src="/icons/orbits.png">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium font-normal" style="padding-right: 3px;" title="Replication hops">{{ sbot_config.hops }}</label>
</div>
<label class="label-small font-gray">HOPS</label>
</div>
<div class="stack">
<img class="icon icon-active" title="Blobs" src="/icons/image-file.png">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium font-normal" style="padding-right: 3px;" title="Blobstore size in MB">{{ blobs }}</label>
<label class="label-small font-normal">MB</label>
</div>
<label class="label-small font-gray">BLOBSTORE</label>
</div>
<div class="stack">
<img class="icon{% if sbot_status.memory %} icon-active{% else %} icon-inactive{% endif %}" title="Memory" src="/icons/ram.png">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium{% if sbot_status.state == "inactive" %} font-gray{% else %} font-normal{% endif %}" style="padding-right: 3px;" title="Memory usage of the go-sbot process in MB">{{ mem }}</label>
<label class="label-small{% if sbot_status.state == "inactive" %} font-gray{% else %} font-normal{% endif %}">MB</label>
</div>
<label class="label-small font-gray">MEMORY</label>
</div>
</div>
</div>
</div>
*/
}
}
};
// 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)
}

View File

@ -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<RocketConfig>) -> 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)
}
}
}

View File

@ -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<String>) -> PreEscaped<String> {
pub fn build_template(body: PreEscaped<String>) -> PreEscaped<String> {
html! {
(DOCTYPE)
html lang="en" data-theme="light";

View File

@ -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",

View File

@ -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<String>, title: &str, back: Option<&str>) -> PreEscaped<String> {
pub fn build_template(
main: PreEscaped<String>,
title: &str,
back: Option<&str>,
) -> PreEscaped<String> {
// retrieve the current theme value
let theme = utils::get_theme();
@ -38,7 +42,7 @@ pub fn build(main: PreEscaped<String>, 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";
}
}