use crate::{ components::router::{AppRoute, Link}, infra::{ api::HostService, common_component::{CommonComponent, CommonComponentParts}, }, }; use anyhow::{anyhow, bail, Result}; use gloo_console::error; use lldap_auth::*; use validator_derive::Validate; use yew::prelude::*; use yew_form::Form; use yew_form_derive::Model; use yew_router::{prelude::History, scope_ext::RouterScopeExt}; #[derive(PartialEq, Eq, Default)] enum OpaqueData { #[default] None, Login(opaque::client::login::ClientLogin), Registration(opaque::client::registration::ClientRegistration), } impl OpaqueData { fn take(&mut self) -> Self { std::mem::take(self) } } /// The fields of the form, with the constraints. #[derive(Model, Validate, PartialEq, Eq, 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 { common: CommonComponentParts, form: Form, opaque_data: OpaqueData, } #[derive(Clone, PartialEq, Eq, Properties)] pub struct Props { pub username: String, pub is_admin: bool, } pub enum Msg { FormUpdate, Submit, AuthenticationStartResponse(Result>), SubmitNewPassword, RegistrationStartResponse(Result>), RegistrationFinishResponse(Result<()>), } impl CommonComponent for ChangePasswordForm { fn handle_msg( &mut self, ctx: &Context, msg: ::Message, ) -> Result { use anyhow::Context; match msg { Msg::FormUpdate => Ok(true), Msg::Submit => { if !self.form.validate() { bail!("Check the form for errors"); } if ctx.props().is_admin { self.handle_msg(ctx, 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: ctx.props().username.clone().into(), login_start_request: login_start_request.message, }; self.common.call_backend( ctx, HostService::login_start(req), Msg::AuthenticationStartResponse, ); Ok(true) } } Msg::AuthenticationStartResponse(res) => { let res = res.context("Could not initiate login")?; match self.opaque_data.take() { OpaqueData::Login(l) => { opaque::client::login::finish_login(l, res.credential_response).map_err( |e| { // Common error, we want to print a full error to the console but only a // simple one to the user. error!(&format!("Invalid username or password: {}", e)); anyhow!("Invalid username or password") }, )?; } _ => panic!("Unexpected data in opaque_data field"), }; self.handle_msg(ctx, Msg::SubmitNewPassword) } Msg::SubmitNewPassword => { let mut rng = rand::rngs::OsRng; let new_password = self.form.model().password; let registration_start_request = opaque::client::registration::start_registration( new_password.as_bytes(), &mut rng, ) .context("Could not initiate password change")?; let req = registration::ClientRegistrationStartRequest { username: ctx.props().username.clone().into(), registration_start_request: registration_start_request.message, }; self.opaque_data = OpaqueData::Registration(registration_start_request.state); self.common.call_backend( ctx, HostService::register_start(req), Msg::RegistrationStartResponse, ); Ok(true) } Msg::RegistrationStartResponse(res) => { let res = res.context("Could not initiate password change")?; match self.opaque_data.take() { OpaqueData::Registration(registration) => { let mut rng = rand::rngs::OsRng; let registration_finish = opaque::client::registration::finish_registration( registration, res.registration_response, &mut rng, ) .context("Error during password change")?; let req = registration::ClientRegistrationFinishRequest { server_data: res.server_data, registration_upload: registration_finish.message, }; self.common.call_backend( ctx, HostService::register_finish(req), Msg::RegistrationFinishResponse, ); } _ => panic!("Unexpected data in opaque_data field"), }; Ok(false) } Msg::RegistrationFinishResponse(response) => { if response.is_ok() { ctx.link().history().unwrap().push(AppRoute::UserDetails { user_id: ctx.props().username.clone(), }); } response?; Ok(true) } } } fn mut_common(&mut self) -> &mut CommonComponentParts { &mut self.common } } impl Component for ChangePasswordForm { type Message = Msg; type Properties = Props; fn create(_: &Context) -> Self { ChangePasswordForm { common: CommonComponentParts::::create(), form: yew_form::Form::::new(FormModel::default()), opaque_data: OpaqueData::None, } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { CommonComponentParts::::update(self, ctx, msg) } fn view(&self, ctx: &Context) -> Html { let is_admin = ctx.props().is_admin; let link = ctx.link(); type Field = yew_form::Field; html! { <>
{"Change password"}
{ if let Some(e) = &self.common.error { html! {
{e.to_string() }
} } else { html! {} } }
{if !is_admin { html! {
{&self.form.field_message("old_password")}
}} else { html! {} }}
{&self.form.field_message("password")}
{&self.form.field_message("confirm_password")}
{"Back"}
} } }