forked from PeachCloud/peach-workspace
435 lines
16 KiB
Rust
435 lines
16 KiB
Rust
use log::{info};
|
|
use rocket::request::{FlashMessage};
|
|
use rocket::form::{Form, FromForm};
|
|
use rocket::response::{Flash, Redirect};
|
|
use rocket::{get, post};
|
|
use rocket::serde::json::Json;
|
|
use rocket_dyn_templates::Template;
|
|
use rocket::serde::{Deserialize, Serialize};
|
|
|
|
use peach_lib::password_utils;
|
|
use peach_lib::error::PeachError;
|
|
|
|
use crate::error::PeachWebError;
|
|
use crate::utils::{build_json_response, TemplateOrRedirect};
|
|
use rocket::serde::json::Value;
|
|
use rocket::request::{self, FromRequest, Request};
|
|
use rocket::http::{Cookie, CookieJar, Status};
|
|
|
|
|
|
|
|
|
|
// 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> {
|
|
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
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct LoginContext {
|
|
pub back: Option<String>,
|
|
pub flash_name: Option<String>,
|
|
pub flash_msg: Option<String>,
|
|
pub title: Option<String>,
|
|
}
|
|
|
|
impl LoginContext {
|
|
pub fn build() -> LoginContext {
|
|
LoginContext {
|
|
back: None,
|
|
flash_name: None,
|
|
flash_msg: None,
|
|
title: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[get("/login")]
|
|
pub fn login(flash: Option<FlashMessage>) -> Template {
|
|
let mut context = LoginContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Login".to_string());
|
|
// check to see if there is a flash message to display
|
|
if let Some(flash) = flash {
|
|
// add flash message contents to the context object
|
|
context.flash_name = Some(flash.kind().to_string());
|
|
context.flash_msg = Some(flash.message().to_string());
|
|
};
|
|
Template::render("login", &context)
|
|
}
|
|
|
|
|
|
#[derive(Debug, Deserialize, FromForm)]
|
|
pub struct LoginForm {
|
|
pub username: String,
|
|
pub password: String,
|
|
}
|
|
|
|
/// Takes in a LoginForm and returns Ok(()) if username and password
|
|
/// are correct to authenticate with peach-web.
|
|
///
|
|
/// Note: currently there is only one user, and the username should always
|
|
/// be "admin".
|
|
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 {
|
|
let result = verify_login_form(login_form.into_inner());
|
|
match result {
|
|
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(_) => {
|
|
// if unsuccessful login, render /login page again
|
|
let mut context = LoginContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Login".to_string());
|
|
context.flash_name = Some("error".to_string());
|
|
let flash_msg = "Invalid password".to_string();
|
|
context.flash_msg = Some(flash_msg);
|
|
TemplateOrRedirect::Template(Template::render("login", &context))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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, Deserialize, FromForm)]
|
|
pub struct ResetPasswordForm {
|
|
pub temporary_password: String,
|
|
pub new_password1: String,
|
|
pub new_password2: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ResetPasswordContext {
|
|
pub back: Option<String>,
|
|
pub title: Option<String>,
|
|
pub flash_name: Option<String>,
|
|
pub flash_msg: Option<String>,
|
|
}
|
|
|
|
impl ResetPasswordContext {
|
|
pub fn build() -> ResetPasswordContext {
|
|
ResetPasswordContext {
|
|
back: None,
|
|
title: None,
|
|
flash_name: None,
|
|
flash_msg: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ChangePasswordContext {
|
|
pub back: Option<String>,
|
|
pub title: Option<String>,
|
|
pub flash_name: Option<String>,
|
|
pub flash_msg: Option<String>,
|
|
}
|
|
|
|
impl ChangePasswordContext {
|
|
pub fn build() -> ChangePasswordContext {
|
|
ChangePasswordContext {
|
|
back: None,
|
|
title: None,
|
|
flash_name: None,
|
|
flash_msg: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password.
|
|
pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> {
|
|
info!(
|
|
"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.
|
|
/// All routes under /public/* are excluded from nginx basic auth via the nginx config.
|
|
#[get("/reset_password")]
|
|
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
|
|
let mut context = ResetPasswordContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Reset Password".to_string());
|
|
// check to see if there is a flash message to display
|
|
if let Some(flash) = flash {
|
|
// add flash message contents to the context object
|
|
context.flash_name = Some(flash.kind().to_string());
|
|
context.flash_msg = Some(flash.message().to_string());
|
|
};
|
|
Template::render("password/reset_password", &context)
|
|
}
|
|
|
|
/// 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.
|
|
/// This route is excluded from nginx basic auth via the nginx config.
|
|
#[post("/reset_password", data = "<reset_password_form>")]
|
|
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
|
|
let result = save_reset_password_form(reset_password_form.into_inner());
|
|
match result {
|
|
Ok(_) => {
|
|
let mut context = ChangePasswordContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Reset Password".to_string());
|
|
context.flash_name = Some("success".to_string());
|
|
let flash_msg = "New password is now saved. Return home to login".to_string();
|
|
context.flash_msg = Some(flash_msg);
|
|
Template::render("password/reset_password", &context)
|
|
}
|
|
Err(err) => {
|
|
let mut context = ChangePasswordContext::build();
|
|
// set back icon link to network route
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Reset Password".to_string());
|
|
context.flash_name = Some("error".to_string());
|
|
context.flash_msg = Some(format!("Failed to reset password: {}", err));
|
|
Template::render("password/reset_password", &context)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
/// All routes under /public/* are excluded from nginx basic auth via the nginx config.
|
|
#[post("/public/api/v1/reset_password", data = "<reset_password_form>")]
|
|
pub fn reset_password_form_endpoint(
|
|
reset_password_form: Json<ResetPasswordForm>,
|
|
) -> Value {
|
|
let result = save_reset_password_form(reset_password_form.into_inner());
|
|
match result {
|
|
Ok(_) => {
|
|
let status = "success".to_string();
|
|
let msg = "New password is now saved. Return home to login.".to_string();
|
|
build_json_response(status, None, Some(msg))
|
|
}
|
|
Err(err) => {
|
|
let status = "error".to_string();
|
|
let msg = format!("{}", err);
|
|
build_json_response(status, None, Some(msg))
|
|
}
|
|
}
|
|
}
|
|
|
|
// HELPERS AND ROUTES FOR /send_password_reset
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct SendPasswordResetContext {
|
|
pub back: Option<String>,
|
|
pub title: Option<String>,
|
|
pub flash_name: Option<String>,
|
|
pub flash_msg: Option<String>,
|
|
}
|
|
|
|
impl SendPasswordResetContext {
|
|
pub fn build() -> SendPasswordResetContext {
|
|
SendPasswordResetContext {
|
|
back: None,
|
|
title: None,
|
|
flash_name: None,
|
|
flash_msg: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Password reset request handler. This route is used by a user who is not logged in to send a new password reset link.
|
|
#[get("/send_password_reset")]
|
|
pub fn send_password_reset_page(flash: Option<FlashMessage>) -> Template {
|
|
let mut context = SendPasswordResetContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Send Password Reset".to_string());
|
|
// check to see if there is a flash message to display
|
|
if let Some(flash) = flash {
|
|
// add flash message contents to the context object
|
|
context.flash_name = Some(flash.kind().to_string());
|
|
context.flash_msg = Some(flash.message().to_string());
|
|
};
|
|
Template::render("password/send_password_reset", &context)
|
|
}
|
|
|
|
/// 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 result = password_utils::send_password_reset();
|
|
match result {
|
|
Ok(_) => {
|
|
let mut context = ChangePasswordContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Send Password Reset".to_string());
|
|
context.flash_name = Some("success".to_string());
|
|
let flash_msg =
|
|
"A password reset link has been sent to the admin of this device".to_string();
|
|
context.flash_msg = Some(flash_msg);
|
|
Template::render("password/send_password_reset", &context)
|
|
}
|
|
Err(err) => {
|
|
let mut context = ChangePasswordContext::build();
|
|
context.back = Some("/".to_string());
|
|
context.title = Some("Send Password Reset".to_string());
|
|
context.flash_name = Some("error".to_string());
|
|
context.flash_msg = Some(format!("Failed to send password reset link: {}", err));
|
|
Template::render("password/send_password_reset", &context)
|
|
}
|
|
}
|
|
}
|
|
|
|
// HELPERS AND ROUTES FOR /settings/change_password
|
|
|
|
#[derive(Debug, Deserialize, FromForm)]
|
|
pub struct PasswordForm {
|
|
pub old_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.old_password, password_form.new_password1, password_form.new_password2
|
|
);
|
|
password_utils::verify_password(&password_form.old_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("/settings/change_password")]
|
|
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
let mut context = ChangePasswordContext::build();
|
|
// set back icon link to network route
|
|
context.back = Some("/network".to_string());
|
|
context.title = Some("Change Password".to_string());
|
|
// check to see if there is a flash message to display
|
|
if let Some(flash) = flash {
|
|
// add flash message contents to the context object
|
|
context.flash_name = Some(flash.kind().to_string());
|
|
context.flash_msg = Some(flash.message().to_string());
|
|
};
|
|
Template::render("password/change_password", &context)
|
|
}
|
|
|
|
/// Change password form request handler. This route is used by a user who is already logged in.
|
|
#[post("/settings/change_password", data = "<password_form>")]
|
|
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
|
|
let result = save_password_form(password_form.into_inner());
|
|
match result {
|
|
Ok(_) => {
|
|
let mut context = ChangePasswordContext::build();
|
|
// set back icon link to network route
|
|
context.back = Some("/network".to_string());
|
|
context.title = Some("Change Password".to_string());
|
|
context.flash_name = Some("success".to_string());
|
|
context.flash_msg = Some("New password is now saved".to_string());
|
|
// template_dir is set in Rocket.toml
|
|
Template::render("password/change_password", &context)
|
|
}
|
|
Err(err) => {
|
|
let mut context = ChangePasswordContext::build();
|
|
// set back icon link to network route
|
|
context.back = Some("/network".to_string());
|
|
context.title = Some("Configure DNS".to_string());
|
|
context.flash_name = Some("error".to_string());
|
|
context.flash_msg = Some(format!("Failed to save new password: {}", err));
|
|
Template::render("password/change_password", &context)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// JSON change password form request handler.
|
|
#[post("/api/v1/settings/change_password", data = "<password_form>")]
|
|
pub fn save_password_form_endpoint(password_form: Json<PasswordForm>, _auth: Authenticated) -> Value {
|
|
let result = save_password_form(password_form.into_inner());
|
|
match result {
|
|
Ok(_) => {
|
|
let status = "success".to_string();
|
|
let msg = "Your password was successfully changed".to_string();
|
|
build_json_response(status, None, Some(msg))
|
|
}
|
|
Err(err) => {
|
|
let status = "error".to_string();
|
|
let msg = format!("{}", err);
|
|
build_json_response(status, None, Some(msg))
|
|
}
|
|
}
|
|
}
|