From fa6427e694942de8ae7a9ec2044ea2ee461143c1 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 19 Sep 2021 20:21:37 +0200 Subject: [PATCH] app: Migrate login to yew_form --- app/src/components/login.rs | 172 ++++++++++++++--------------- app/src/components/user_details.rs | 4 +- 2 files changed, 85 insertions(+), 91 deletions(-) diff --git a/app/src/components/login.rs b/app/src/components/login.rs index 81e5f27..3b125ca 100644 --- a/app/src/components/login.rs +++ b/app/src/components/login.rs @@ -1,114 +1,100 @@ use crate::infra::api::HostService; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use lldap_auth::*; -use wasm_bindgen::JsCast; +use validator_derive::Validate; use yew::prelude::*; use yew::services::{fetch::FetchTask, ConsoleService}; +use yew_form::Form; +use yew_form_derive::Model; pub struct LoginForm { link: ComponentLink, on_logged_in: Callback<(String, bool)>, error: Option, - node_ref: NodeRef, - login_start: Option, + form: Form, // Used to keep the request alive long enough. _task: Option, } +/// The fields of the form, with the constraints. +#[derive(Model, Validate, PartialEq, Clone, Default)] +pub struct FormModel { + #[validate(length(min = 1, message = "Missing username"))] + username: String, + #[validate(length(min = 8, message = "Invalid password. Min length: 8"))] + password: String, +} + #[derive(Clone, PartialEq, Properties)] pub struct Props { pub on_logged_in: Callback<(String, bool)>, } pub enum Msg { + Update, Submit, - AuthenticationStartResponse(Result>), + AuthenticationStartResponse( + ( + opaque::client::login::ClientLogin, + Result>, + ), + ), AuthenticationFinishResponse(Result<(String, bool)>), } -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(), - ) -} - impl LoginForm { - 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))?); - Ok(()) - } - - fn handle_message(&mut self, msg: ::Message) -> Result<()> { + fn handle_message(&mut self, msg: ::Message) -> Result { match msg { + Msg::Update => Ok(true), Msg::Submit => { - let username = get_form_field("username") - .ok_or_else(|| anyhow!("Could not get username from form"))?; - let password = get_form_field("password") - .ok_or_else(|| anyhow!("Could not get password from form"))?; + if !self.form.validate() { + bail!("Invalid inputs"); + } + let FormModel { username, password } = self.form.model(); let mut rng = rand::rngs::OsRng; - let login_start_request = opaque::client::login::start_login(&password, &mut rng) - .context("Could not initialize login")?; - self.login_start = Some(login_start_request.state); + let opaque::client::login::ClientLoginStartResult { state, message } = + opaque::client::login::start_login(&password, &mut rng) + .context("Could not initialize login")?; let req = login::ClientLoginStartRequest { username, - login_start_request: login_start_request.message, + login_start_request: message, }; - self.call_backend( - HostService::login_start, + self._task = Some(HostService::login_start( req, - Msg::AuthenticationStartResponse, - )?; - Ok(()) + self.link + .callback_once(move |r| Msg::AuthenticationStartResponse((state, r))), + )?); + Ok(false) } - Msg::AuthenticationStartResponse(Ok(res)) => { - debug_assert!(self.login_start.is_some()); - let login_finish = match opaque::client::login::finish_login( - self.login_start.as_ref().unwrap().clone(), - res.credential_response, - ) { - Err(e) => { - // Common error, we want to print a full error to the console but only a - // simple one to the user. - ConsoleService::error(&format!("Invalid username or password: {}", e)); - self.error = Some(anyhow!("Invalid username or password")); - return Ok(()); - } - Ok(l) => l, - }; + Msg::AuthenticationStartResponse((login_start, res)) => { + let res = res.context("Could not log in (invalid response to login start)")?; + let login_finish = + match opaque::client::login::finish_login(login_start, res.credential_response) + { + Err(e) => { + // Common error, we want to print a full error to the console but only a + // simple one to the user. + ConsoleService::error(&format!("Invalid username or password: {}", e)); + self.error = Some(anyhow!("Invalid username or password")); + return Ok(true); + } + Ok(l) => l, + }; let req = login::ClientLoginFinishRequest { server_data: res.server_data, credential_finalization: login_finish.message, }; - self.call_backend( - HostService::login_finish, + self._task = Some(HostService::login_finish( req, - Msg::AuthenticationFinishResponse, - )?; - Ok(()) + self.link.callback_once(Msg::AuthenticationFinishResponse), + )?); + Ok(false) } - Msg::AuthenticationStartResponse(Err(e)) => Err(anyhow!( - "Could not log in (invalid response to login start): {}", - e - )), - Msg::AuthenticationFinishResponse(Ok(user_info)) => { - self.on_logged_in.emit(user_info); - Ok(()) + Msg::AuthenticationFinishResponse(user_info) => { + self.on_logged_in + .emit(user_info.context("Could not log in")?); + Ok(true) } - Msg::AuthenticationFinishResponse(Err(e)) => Err(anyhow!("Could not log in: {}", e)), } } } @@ -122,18 +108,21 @@ impl Component for LoginForm { link, on_logged_in: props.on_logged_in, error: None, - node_ref: NodeRef::default(), - login_start: None, + form: Form::::new(FormModel::default()), _task: None, } } fn update(&mut self, msg: Self::Message) -> ShouldRender { 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); + true + } + Ok(b) => b, } - true } fn change(&mut self, _: Self::Properties) -> ShouldRender { @@ -141,23 +130,25 @@ impl Component for LoginForm { } fn view(&self) -> Html { + type Field = yew_form::Field; html! {
+ class="form center-block col-sm-4 col-offset-4">
- + autocomplete="username" + oninput=self.link.callback(|_| Msg::Update) />
@@ -165,18 +156,21 @@ impl Component for LoginForm {
-
diff --git a/app/src/components/user_details.rs b/app/src/components/user_details.rs index 50017db..4e8067f 100644 --- a/app/src/components/user_details.rs +++ b/app/src/components/user_details.rs @@ -93,7 +93,7 @@ impl UserDetails { fn view_messages(&self, error: &Option) -> Html { if let Some(e) = error { html! { -
+
{"Error: "}{e.to_string()}
} @@ -205,7 +205,6 @@ impl Component for UserDetails { - {self.view_messages(error)} {self.view_group_memberships(u)}
+ {self.view_messages(error)}
} }