From ead501158d36f4f3fbf5274ff1a434ce3a9b46d8 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Tue, 12 Oct 2021 05:02:20 +0200 Subject: [PATCH] app: Migrate change password to yew_form Also disable submit while the password is being sent. --- app/src/components/change_password.rs | 252 ++++++++++++++++---------- app/src/components/create_user.rs | 2 +- app/src/components/login.rs | 8 +- 3 files changed, 162 insertions(+), 100 deletions(-) diff --git a/app/src/components/change_password.rs b/app/src/components/change_password.rs index e0b6343..b591ba2 100644 --- a/app/src/components/change_password.rs +++ b/app/src/components/change_password.rs @@ -4,11 +4,17 @@ use crate::{ }; use anyhow::{anyhow, bail, Context, Result}; use lldap_auth::*; -use wasm_bindgen::JsCast; +use validator_derive::Validate; use yew::{ prelude::*, services::{fetch::FetchTask, ConsoleService}, }; +use yew_form::Form; +use yew_form_derive::Model; +use yew_router::{ + agent::{RouteAgentDispatcher, RouteRequest}, + route::Route, +}; #[derive(PartialEq, Eq)] enum OpaqueData { @@ -29,16 +35,37 @@ impl OpaqueData { } } +/// The fields of the form, with the constraints. +#[derive(Model, Validate, PartialEq, Clone, Default)] +pub struct FormModel { + #[validate(custom( + function = "empty_or_long", + message = "Password should be longer than 8 characters" + ))] + old_password: String, + #[validate(length(min = 8, message = "Invalid password. Min length: 8"))] + password: String, + #[validate(must_match(other = "password", message = "Passwords must match"))] + confirm_password: String, +} + +fn empty_or_long(value: &str) -> Result<(), validator::ValidationError> { + if value.is_empty() || value.len() >= 8 { + Ok(()) + } else { + Err(validator::ValidationError::new("")) + } +} + pub struct ChangePasswordForm { link: ComponentLink, - username: String, + props: Props, error: Option, - node_ref: NodeRef, + form: Form, opaque_data: OpaqueData, - successfully_changed_password: bool, - is_admin: bool, // Used to keep the request alive long enough. - _task: Option, + task: Option, + route_dispatcher: RouteAgentDispatcher, } #[derive(Clone, PartialEq, Properties)] @@ -48,6 +75,7 @@ pub struct Props { } pub enum Msg { + FormUpdate, Submit, AuthenticationStartResponse(Result>), SubmitNewPassword, @@ -55,71 +83,37 @@ pub enum Msg { RegistrationFinishResponse(Result<()>), } -fn get_form_field(field_id: &str) -> Option { - let document = web_sys::window()?.document()?; - Some( - document - .get_element_by_id(field_id)? - .dyn_into::() - .ok()? - .value(), - ) -} - -fn clear_form_fields() -> Option<()> { - let document = web_sys::window()?.document()?; - - let clear_field = |id| { - document - .get_element_by_id(id)? - .dyn_into::() - .ok()? - .set_value(""); - Some(()) - }; - clear_field("oldPassword"); - clear_field("newPassword"); - clear_field("confirmPassword"); - None -} - impl ChangePasswordForm { - fn set_error(&mut self, error: anyhow::Error) { - ConsoleService::error(&error.to_string()); - self.error = Some(error); - } - fn call_backend(&mut self, method: M, req: Req, callback: C) -> Result<()> where M: Fn(Req, Callback) -> Result, C: Fn(Resp) -> ::Message + 'static, { - self._task = Some(method(req, self.link.callback(callback))?); + self.task = Some(method(req, self.link.callback(callback))?); Ok(()) } - fn handle_message(&mut self, msg: ::Message) -> Result<()> { + fn handle_message(&mut self, msg: ::Message) -> Result { match msg { + Msg::FormUpdate => Ok(true), Msg::Submit => { - let old_password = get_form_field("oldPassword") - .ok_or_else(|| anyhow!("Could not get old password from form"))?; - let new_password = get_form_field("newPassword") - .ok_or_else(|| anyhow!("Could not get new password from form"))?; - let confirm_password = get_form_field("confirmPassword") - .ok_or_else(|| anyhow!("Could not get confirmation password from form"))?; - if new_password != confirm_password { - bail!("Confirmation password doesn't match"); + if !self.form.validate() { + bail!("Check the form for errors"); } - if self.is_admin { + if self.props.is_admin { self.handle_message(Msg::SubmitNewPassword) } else { + let old_password = self.form.model().old_password; + if old_password.is_empty() { + bail!("Current password should not be empty"); + } let mut rng = rand::rngs::OsRng; let login_start_request = opaque::client::login::start_login(&old_password, &mut rng) .context("Could not initialize login")?; self.opaque_data = OpaqueData::Login(login_start_request.state); let req = login::ClientLoginStartRequest { - username: self.username.clone(), + username: self.props.username.clone(), login_start_request: login_start_request.message, }; self.call_backend( @@ -127,7 +121,7 @@ impl ChangePasswordForm { req, Msg::AuthenticationStartResponse, )?; - Ok(()) + Ok(true) } } Msg::AuthenticationStartResponse(res) => { @@ -152,13 +146,12 @@ impl ChangePasswordForm { } Msg::SubmitNewPassword => { let mut rng = rand::rngs::OsRng; - let new_password = get_form_field("newPassword") - .ok_or_else(|| anyhow!("Could not get new password from form"))?; + let new_password = self.form.model().password; let registration_start_request = opaque::client::registration::start_registration(&new_password, &mut rng) .context("Could not initiate password change")?; let req = registration::ClientRegistrationStartRequest { - username: self.username.clone(), + username: self.props.username.clone(), registration_start_request: registration_start_request.message, }; self.opaque_data = OpaqueData::Registration(registration_start_request.state); @@ -167,7 +160,7 @@ impl ChangePasswordForm { req, Msg::RegistrationStartResponse, )?; - Ok(()) + Ok(true) } Msg::RegistrationStartResponse(res) => { let res = res.context("Could not initiate password change")?; @@ -192,14 +185,19 @@ impl ChangePasswordForm { ) } _ => panic!("Unexpected data in opaque_data field"), - } + }?; + Ok(false) } Msg::RegistrationFinishResponse(response) => { + self.task = None; if response.is_ok() { - self.successfully_changed_password = true; - clear_form_fields(); + self.route_dispatcher + .send(RouteRequest::ChangeRoute(Route::from( + AppRoute::UserDetails(self.props.username.clone()), + ))); } - response + response?; + Ok(true) } } } @@ -212,23 +210,26 @@ impl Component for ChangePasswordForm { fn create(props: Self::Properties, link: ComponentLink) -> Self { ChangePasswordForm { link, - username: props.username, + props, error: None, - node_ref: NodeRef::default(), + form: yew_form::Form::::new(FormModel::default()), opaque_data: OpaqueData::None, - successfully_changed_password: false, - is_admin: props.is_admin, - _task: None, + task: None, + route_dispatcher: RouteAgentDispatcher::new(), } } fn update(&mut self, msg: Self::Message) -> ShouldRender { - self.successfully_changed_password = false; self.error = None; - if let Err(e) = self.handle_message(msg) { - self.set_error(e); + match self.handle_message(msg) { + Err(e) => { + ConsoleService::error(&e.to_string()); + self.error = Some(e); + self.task = None; + true + } + Ok(b) => b, } - true } fn change(&mut self, _: Self::Properties) -> ShouldRender { @@ -236,38 +237,97 @@ impl Component for ChangePasswordForm { } fn view(&self) -> Html { - let is_admin = self.is_admin; + let is_admin = self.props.is_admin; + type Field = yew_form::Field; html! { -
-
- - + <> + + {if !is_admin { html! { +
+ +
+ +
+ {&self.form.field_message("old_password")} +
+
-
- - + }} else { html! {} }} +
+ +
+ +
+ {&self.form.field_message("password")} +
-
- - -
- -
- { if let Some(e) = &self.error { - html! { e.to_string() } - } else if self.successfully_changed_password { - html! { -
- {"Successfully changed the password"} -
- } - } else { html! {} } - } -
-
- {"Back"} +
+
+ +
+ +
+ {&self.form.field_message("confirm_password")} +
+
+
+ +
+ { if let Some(e) = &self.error { + html! { +
+ {e.to_string() } +
+ } + } else { html! {} } + } +
+ + {"Back"} + +
+ } } } diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs index 3dd59bd..0540f5f 100644 --- a/app/src/components/create_user.rs +++ b/app/src/components/create_user.rs @@ -340,7 +340,7 @@ impl Component for CreateUserForm {
diff --git a/app/src/components/login.rs b/app/src/components/login.rs index eb65d71..f66349a 100644 --- a/app/src/components/login.rs +++ b/app/src/components/login.rs @@ -2,8 +2,10 @@ use crate::infra::api::HostService; use anyhow::{anyhow, bail, Context, Result}; use lldap_auth::*; use validator_derive::Validate; -use yew::prelude::*; -use yew::services::{fetch::FetchTask, ConsoleService}; +use yew::{ + prelude::*, + services::{fetch::FetchTask, ConsoleService}, +}; use yew_form::Form; use yew_form_derive::Model; @@ -48,7 +50,7 @@ impl LoginForm { Msg::Update => Ok(true), Msg::Submit => { if !self.form.validate() { - bail!("Invalid inputs"); + bail!("Check the form for errors"); } let FormModel { username, password } = self.form.model(); let mut rng = rand::rngs::OsRng;