From dfe1607a3edbfe63e06c1674ceae7488498fb067 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 19 Sep 2021 21:02:23 +0200 Subject: [PATCH] app: Migrate create_user to yew_form --- Cargo.lock | 2 - app/Cargo.toml | 2 - app/src/components/create_user.rs | 239 ++++++++++++++++-------- app/src/components/user_details_form.rs | 6 +- 4 files changed, 161 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3d7414..c4a0988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1756,10 +1756,8 @@ dependencies = [ "graphql_client", "http", "jwt", - "lazy_static", "lldap_auth", "rand 0.8.4", - "regex", "serde", "serde_json", "validator", diff --git a/app/Cargo.toml b/app/Cargo.toml index 5e61f0e..ed149ba 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -9,9 +9,7 @@ anyhow = "1" graphql_client = "0.10" http = "0.2" jwt = "0.13" -lazy_static = "*" rand = "0.8" -regex = "*" serde = "1" serde_json = "1" validator = "*" diff --git a/app/src/components/create_user.rs b/app/src/components/create_user.rs index 905c89e..dae42df 100644 --- a/app/src/components/create_user.rs +++ b/app/src/components/create_user.rs @@ -1,9 +1,11 @@ use crate::infra::api::HostService; -use anyhow::{anyhow, Context, Result}; +use anyhow::{bail, Context, Result}; use graphql_client::GraphQLQuery; use lldap_auth::{opaque, registration}; +use validator_derive::Validate; use yew::prelude::*; use yew::services::{fetch::FetchTask, ConsoleService}; +use yew_form_derive::Model; use yew_router::{ agent::{RouteAgentDispatcher, RouteRequest}, route::Route, @@ -21,41 +23,70 @@ pub struct CreateUser; pub struct CreateUserForm { link: ComponentLink, route_dispatcher: RouteAgentDispatcher, - node_ref: NodeRef, + form: yew_form::Form, error: Option, - registration_start: Option, // Used to keep the request alive long enough. _task: Option, } +#[derive(Model, Validate, PartialEq, Clone, Default)] +pub struct CreateUserModel { + #[validate(length(min = 1, message = "Username is required"))] + username: String, + #[validate(email)] + email: String, + #[validate(length(min = 1, message = "Display name is required"))] + display_name: String, + first_name: String, + last_name: String, + #[validate(custom( + function = "empty_or_long", + message = "Password should be longer than 8 characters (or left empty)" + ))] + 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 enum Msg { - CreateUserResponse(Result), + Update, SubmitForm, + CreateUserResponse(Result), SuccessfulCreation, - RegistrationStartResponse(Result>), + RegistrationStartResponse( + ( + opaque::client::registration::ClientRegistration, + Result>, + ), + ), RegistrationFinishResponse(Result<()>), } -#[allow(clippy::ptr_arg)] -fn not_empty(s: &String) -> bool { - !s.is_empty() -} - impl CreateUserForm { - fn handle_msg(&mut self, msg: ::Message) -> Result<()> { + fn handle_msg(&mut self, msg: ::Message) -> Result { match msg { + Msg::Update => Ok(true), Msg::SubmitForm => { + if !self.form.validate() { + bail!("Check the form for errors"); + } + let model = self.form.model(); + let to_option = |s: String| if s.is_empty() { None } else { Some(s) }; let req = create_user::Variables { user: create_user::CreateUserInput { - id: get_element("username") - .filter(not_empty) - .ok_or_else(|| anyhow!("Missing username"))?, - email: get_element("email") - .filter(not_empty) - .ok_or_else(|| anyhow!("Missing email"))?, - displayName: get_element("display-name").filter(not_empty), - firstName: get_element("first-name").filter(not_empty), - lastName: get_element("last-name").filter(not_empty), + id: model.username, + email: model.email, + displayName: to_option(model.display_name), + firstName: to_option(model.first_name), + lastName: to_option(model.last_name), }, }; self._task = Some(HostService::graphql_query::( @@ -63,6 +94,7 @@ impl CreateUserForm { self.link.callback(Msg::CreateUserResponse), "Error trying to create user", )?); + Ok(true) } Msg::CreateUserResponse(r) => { match r { @@ -72,36 +104,38 @@ impl CreateUserForm { &r.create_user.id, &r.create_user.creation_date )), }; - let user_id = get_element("username") - .filter(not_empty) - .ok_or_else(|| anyhow!("Missing username"))?; - if let Some(password) = get_element("password").filter(not_empty) { + let model = self.form.model(); + let user_id = model.username; + let password = model.password; + if !password.is_empty() { // User was successfully created, let's register the password. let mut rng = rand::rngs::OsRng; - let client_registration_start = - opaque::client::registration::start_registration(&password, &mut rng)?; - self.registration_start = Some(client_registration_start.state); + let opaque::client::registration::ClientRegistrationStartResult { + state, + message, + } = opaque::client::registration::start_registration(&password, &mut rng)?; let req = registration::ClientRegistrationStartRequest { username: user_id, - registration_start_request: client_registration_start.message, + registration_start_request: message, }; self._task = Some( HostService::register_start( req, - self.link.callback(Msg::RegistrationStartResponse), + self.link + .callback_once(move |r| Msg::RegistrationStartResponse((state, r))), ) .context("Error trying to create user")?, ); } else { self.update(Msg::SuccessfulCreation); } + Ok(false) } - Msg::RegistrationStartResponse(response) => { - debug_assert!(self.registration_start.is_some()); + Msg::RegistrationStartResponse((registration_start, response)) => { let response = response?; let mut rng = rand::rngs::OsRng; let registration_upload = opaque::client::registration::finish_registration( - self.registration_start.take().unwrap(), + registration_start, response.registration_response, &mut rng, )?; @@ -116,34 +150,22 @@ impl CreateUserForm { ) .context("Error trying to register user")?, ); + Ok(false) } Msg::RegistrationFinishResponse(response) => { - if response.is_err() { - return response; - } - self.update(Msg::SuccessfulCreation); + response?; + self.handle_msg(Msg::SuccessfulCreation) } Msg::SuccessfulCreation => { self.route_dispatcher .send(RouteRequest::ChangeRoute(Route::new_no_state( "/list_users", ))); + Ok(true) } } - Ok(()) } } -fn get_element(name: &str) -> Option { - use wasm_bindgen::JsCast; - Some( - web_sys::window()? - .document()? - .get_element_by_id(name)? - .dyn_into::() - .ok()? - .value(), - ) -} impl Component for CreateUserForm { type Message = Msg; @@ -153,20 +175,22 @@ impl Component for CreateUserForm { Self { link, route_dispatcher: RouteAgentDispatcher::new(), - node_ref: NodeRef::default(), + form: yew_form::Form::::new(CreateUserModel::default()), error: None, - registration_start: None, _task: None, } } fn update(&mut self, msg: Self::Message) -> ShouldRender { self.error = None; - if let Err(e) = self.handle_msg(msg) { - ConsoleService::error(&e.to_string()); - self.error = Some(e); + match self.handle_msg(msg) { + Err(e) => { + ConsoleService::error(&e.to_string()); + self.error = Some(e); + true + } + Ok(b) => b, } - true } fn change(&mut self, _: Self::Properties) -> ShouldRender { @@ -174,24 +198,28 @@ impl Component for CreateUserForm { } fn view(&self) -> Html { + type Field = yew_form::Field; html! { <>
+ class="form">
- + oninput=self.link.callback(|_| Msg::Update) /> +
+ {&self.form.field_message("username")} +
@@ -200,12 +228,18 @@ impl Component for CreateUserForm { {"Email*:"}
- + oninput=self.link.callback(|_| Msg::Update) /> +
+ {&self.form.field_message("email")} +
@@ -214,12 +248,18 @@ impl Component for CreateUserForm { {"Display name*:"}
- + class_invalid="is-invalid has-error" + class_valid="has-success" + field_name="display_name" + oninput=self.link.callback(|_| Msg::Update) /> +
+ {&self.form.field_message("display_name")}
+
@@ -240,11 +286,17 @@ impl Component for CreateUserForm { {"Last name:"}
- + class_invalid="is-invalid has-error" + class_valid="has-success" + field_name="last_name" + oninput=self.link.callback(|_| Msg::Update) /> +
+ {&self.form.field_message("last_name")} +
@@ -253,18 +305,47 @@ impl Component for CreateUserForm { {"Password:"}
- + oninput=self.link.callback(|_| Msg::Update) /> +
+ {&self.form.field_message("password")} +
+
+
+
+ +
+ +
+ {&self.form.field_message("confirm_password")} +
+ type="button" + onclick=self.link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})> + {"Submit"} +
{ if let Some(e) = &self.error { diff --git a/app/src/components/user_details_form.rs b/app/src/components/user_details_form.rs index 2f3c8b1..d07467e 100644 --- a/app/src/components/user_details_form.rs +++ b/app/src/components/user_details_form.rs @@ -8,14 +8,10 @@ use yew::{ }; use yew_form_derive::Model; -lazy_static::lazy_static! { - static ref EMAIL_RE: regex::Regex = regex::Regex::new("^[^@]+@[^@]+\\.[^@]+$").unwrap(); -} - /// The fields of the form, with the editable details and the constraints. #[derive(Model, Validate, PartialEq, Clone)] pub struct UserModel { - #[validate(regex(path = "EMAIL_RE", message = "Enter a valid email"))] + #[validate(email)] email: String, #[validate(length(min = 1, message = "Display name is required"))] display_name: String,