diff --git a/app/src/lib.rs b/app/src/lib.rs index 9724b54..d04ca80 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -4,7 +4,12 @@ pub mod pages; use pages::list_links; use pages::list_users; +use seed::attrs; +use seed::button; +use seed::input; +use seed::label; use seed::{div, log, prelude::*, App, Url, C}; +use shared::apirequests::users::LoginUser; use shared::datatypes::User; use crate::i18n::{I18n, Lang}; @@ -27,6 +32,8 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { page: Page::init(url, orders, lang.clone()), i18n: lang, user: None, + login_form: LoginForm::default(), + login_data: LoginUser::default(), } } @@ -41,6 +48,14 @@ struct Model { page: Page, i18n: i18n::I18n, user: Option, + login_form: LoginForm, + login_data: LoginUser, +} + +#[derive(Default, Debug)] +struct LoginForm { + username: ElRef, + password: ElRef, } #[derive(Debug)] @@ -83,6 +98,10 @@ pub enum Msg { GetLoggedUser, UserReceived(User), NoMessage, + NotAuthenticated, + Login, + UsernameChanged(String), + PasswordChanged(String), } fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { @@ -102,31 +121,62 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::NoMessage => (), Msg::GetLoggedUser => { - orders.skip(); // No need to rerender/ complicated way to move into the closure + orders.skip(); // No need to rerender orders.perform_cmd(async { - let response = fetch( + // create request + let request = unwrap_or_return!( Request::new("/admin/json/get_logged_user/") .method(Method::Post) - .json(&()) - .expect("serialization failed"), - ) - .await - .expect("HTTP request failed"); - - let user: User = response - .check_status() // ensure we've got 2xx status - .expect("status check failed") - .json() - .await - .expect("deserialization failed"); + .json(&()), + Msg::NotAuthenticated + ); + // perform and get response + let response = unwrap_or_return!(fetch(request).await, Msg::NotAuthenticated); + // validate response status + let response = unwrap_or_return!(response.check_status(), Msg::NotAuthenticated); + let user: User = unwrap_or_return!(response.json().await, Msg::NotAuthenticated); Msg::UserReceived(user) }); } Msg::UserReceived(user) => model.user = Some(user), + Msg::NotAuthenticated => {if model.user.is_some() {model.user = None; logout(orders)}}, + Msg::Login => {login_user(model, orders)} + Msg::UsernameChanged(s) => model.login_data.username = s, + Msg::PasswordChanged(s) => model.login_data.password = s, } } +fn logout(orders: &mut impl Orders) { + orders.perform_cmd(async {let request = Request::new("/admin/logout/"); + unwrap_or_return!(fetch(request).await, Msg::GetLoggedUser); + Msg::NotAuthenticated}); + +} + +fn login_user(model: &mut Model, orders: &mut impl Orders) { + orders.skip(); // No need to rerender + let data = model.login_data.clone(); + + orders.perform_cmd(async { + let data = data; + // create request + let request = unwrap_or_return!( + Request::new("/admin/json/login_user/") + .method(Method::Post) + .json(&data), + Msg::NotAuthenticated + ); + // perform and get response + let response = unwrap_or_return!(fetch(request).await, Msg::NotAuthenticated); + // validate response status + let response = unwrap_or_return!(response.check_status(), Msg::NotAuthenticated); + let user: User = unwrap_or_return!(response.json().await, Msg::NotAuthenticated); + + Msg::UserReceived(user) + }); +} + pub struct Urls<'a> { base_url: std::borrow::Cow<'a, Url>, } @@ -188,8 +238,14 @@ impl<'a> Urls<'a> { fn view(model: &Model) -> Node { div![ C!["page"], - navigation::navigation(&model.i18n, &model.base_url, &model.user), - view_content(&model.page, &model.base_url), + if let Some(user) = &model.user { + div![ + navigation::navigation(&model.i18n, &model.base_url, user), + view_content(&model.page, &model.base_url) + ] + } else { + view_login(&model.i18n, &model) + } ] } @@ -205,6 +261,39 @@ fn view_content(page: &Page, url: &Url) -> Node { ] } +fn view_login(lang: &I18n, model: &Model) -> Node { + let t = move |key: &str| lang.translate(key, None); + + div![ + C!["center", "login"], + div![ + label![t("username")], + input![ + input_ev(Ev::Input, |s| { Msg::UsernameChanged(s) }), + attrs![ + At::Type => "text", + At::Placeholder => t("username"), + At::Name => "username", + At::Value => model.login_data.username], + el_ref(&model.login_form.username) + ] + ], + div![ + label![t("password")], + input![ + input_ev(Ev::Input, |s| { Msg::PasswordChanged(s) }), + attrs![ + At::Type => "password", + At::Placeholder => t("password"), + At::Name => "password", + At::Value => model.login_data.password], + el_ref(&model.login_form.password) + ] + ], + button![t("login"), ev(Ev::Click, |_| Msg::Login)] + ] +} + // ------ ------ // Start // ------ ------ diff --git a/app/src/navigation.rs b/app/src/navigation.rs index 7a7ed19..c4d7375 100644 --- a/app/src/navigation.rs +++ b/app/src/navigation.rs @@ -5,21 +5,17 @@ use shared::datatypes::User; use crate::{i18n::I18n, Msg}; /// Generate the top navigation menu of all pages. -/// +/// /// The menu options are translated using the i18n module. #[must_use] -pub fn navigation(i18n: &I18n, base_url: &Url, user: &Option) -> Node { +pub fn navigation(i18n: &I18n, base_url: &Url, user: &User) -> Node { // A shortcut for translating strings. let t = move |key: &str| i18n.translate(key, None); // Translate the wellcome message - let welcome = if let Some(user) = user { - i18n.translate( - "welcome-user", - Some(&fluent_args![ "username" => user.username.clone()]), - ) - } else { - t("welcome") - }; + let welcome = i18n.translate( + "welcome-user", + Some(&fluent_args![ "username" => user.username.clone()]), + ); nav![ ol![ // A button for the homepage, the list of URLs @@ -57,10 +53,7 @@ pub fn navigation(i18n: &I18n, base_url: &Url, user: &Option) -> Node // The Welcome message li![div![welcome]], // The logout button - li![a![ - attrs! {At::Href => "/admin/logout/"}, - t("logout"), - ]] + li![a![ev(Ev::Click, |_| Msg::NotAuthenticated), t("logout"),]] ] ] } diff --git a/pslink/src/lib.rs b/pslink/src/lib.rs index 0dff6c6..9599dd4 100644 --- a/pslink/src/lib.rs +++ b/pslink/src/lib.rs @@ -281,7 +281,7 @@ pub async fn webservice( .wrap(IdentityService::new( CookieIdentityPolicy::new(&[0; 32]) .name("auth-cookie") - .secure(false), + .secure(true), )) .data(tera.clone()) .service(actix_web_static_files::ResourceFiles::new( @@ -377,6 +377,10 @@ pub async fn webservice( .route( "/get_logged_user/", web::post().to(views::get_logged_user_json), + ) + .route( + "/login_user/", + web::post().to(views::process_login_json), ), ) // login to the admin area diff --git a/pslink/src/models.rs b/pslink/src/models.rs index 14b72d4..bf8e4a7 100644 --- a/pslink/src/models.rs +++ b/pslink/src/models.rs @@ -213,12 +213,6 @@ impl NewUser { } } -#[derive(Debug, Deserialize)] -pub struct LoginUser { - pub username: String, - pub password: String, -} - #[async_trait] pub trait LinkDbOperations { async fn get_link_by_code(code: &str, server_config: &ServerConfig) -> Result; diff --git a/pslink/src/views.rs b/pslink/src/views.rs index c5975e6..fbf7850 100644 --- a/pslink/src/views.rs +++ b/pslink/src/views.rs @@ -17,13 +17,13 @@ use queries::{authenticate, Role}; use shared::apirequests::{ general::{Message, Status}, links::{LinkDelta, LinkRequestForm}, - users::{UserDelta, UserRequestForm}, + users::{LoginUser, UserDelta, UserRequestForm}, }; use tera::{Context, Tera}; use tracing::{error, info, instrument, warn}; use crate::forms::LinkForm; -use crate::models::{LoginUser, NewUser}; +use crate::models::NewUser; use crate::queries; use crate::ServerError; @@ -74,7 +74,6 @@ pub async fn wasm_app(config: web::Data) -> Result - @@ -84,7 +83,6 @@ pub async fn wasm_app(config: web::Data) -> Result Server integration example -
Loading:
- "#, )) } @@ -178,7 +175,7 @@ pub async fn get_logged_user_json( let user = authenticate(&id, &config).await?; match user { Role::NotAuthenticated | Role::Disabled => { - Err(ServerError::User("User not logged in!".to_string())) + Ok(HttpResponse::Unauthorized().finish()) } Role::Regular { user } | Role::Admin { user } => Ok(HttpResponse::Ok().json2(&user)), } @@ -484,7 +481,57 @@ pub async fn process_login( } Err(e) => { info!("Failed to login: {}", e); - Ok(redirect_builder("/admin/login/")) + Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message { + message: "Failed to Login".to_string(), + }))) + } + } +} + +#[instrument(skip(id))] +pub async fn process_login_json( + data: web::Json, + config: web::Data, + id: Identity, +) -> Result { + // query the username to see if a user by that name exists. + let user = queries::get_user_by_name(&data.username, &config).await; + + match user { + Ok(u) => { + // get the password hash + if let Some(hash) = &u.password.secret { + // get the servers secret + let secret = &config.secret; + // validate the secret + let valid = Verifier::default() + .with_hash(hash) + .with_password(&data.password) + .with_secret_key(secret.secret.as_ref().expect("No secret available")) + .verify()?; + + // login the user + if valid { + info!("Log-in of user: {}", &u.username); + let session_token = u.username.clone(); + id.remember(session_token); + Ok(HttpResponse::Ok().json2(&u)) + } else { + info!("Invalid password for user: {}", &u.username); + Ok(redirect_builder("/admin/login/")) + } + } else { + // should fail earlier if secret is missing. + Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message { + message: "Failed to Login".to_string(), + }))) + } + } + Err(e) => { + info!("Failed to login: {}", e); + Ok(HttpResponse::Unauthorized().json2(&Status::Error(Message { + message: "Failed to Login".to_string(), + }))) } } } diff --git a/pslink/static/admin.css b/pslink/static/admin.css index 7168b50..8c1be3a 100644 --- a/pslink/static/admin.css +++ b/pslink/static/admin.css @@ -35,6 +35,16 @@ form { background-color: #eae9ea; } +div.login div { + width: 100%; + height: 60px; + display: table; +} +div.login input { + padding: 15px; + margin-bottom: 20px; +} + .center table p { font-size: x-small; margin: 0; diff --git a/shared/src/apirequests/users.rs b/shared/src/apirequests/users.rs index 9f579d0..68fb1b0 100644 --- a/shared/src/apirequests/users.rs +++ b/shared/src/apirequests/users.rs @@ -25,6 +25,12 @@ impl Default for UserRequestForm { } } +#[derive(Debug, Deserialize, Default, Serialize, Clone)] +pub struct LoginUser { + pub username: String, + pub password: String, +} + /// The Struct that is responsible for creating and editing users. #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct UserDelta {