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