814 lines
25 KiB
Rust
814 lines
25 KiB
Rust
use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc};
|
|
|
|
use const_format::formatcp;
|
|
use dioxus::prelude::*;
|
|
use tracing::{debug, error, warn};
|
|
use validator::{Validate, ValidateArgs, ValidateEmail, ValidationError, ValidationErrors};
|
|
use zxcvbn::zxcvbn;
|
|
|
|
use crate::{
|
|
domain::model::session::Session,
|
|
infrastructure::services::random_svg_generators::{generate_random_svg_shape, ShapeConfig},
|
|
ui::SESSION,
|
|
};
|
|
|
|
use super::{
|
|
button::{LoginButton, RegisterButton},
|
|
modal::{Modal, Severity},
|
|
spinner::Spinner,
|
|
text_input::{PasswordInputState, PasswordTextInput, TextInput, TextInputState},
|
|
};
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/style_tokens.rs"));
|
|
|
|
use style::{
|
|
COLOR_PRIMARY_100, COLOR_PRIMARY_110, COLOR_PRIMARY_120, COLOR_PRIMARY_140, COLOR_PRIMARY_150,
|
|
COLOR_PRIMARY_80, COLOR_PRIMARY_90, COLOR_SECONDARY_100, COLOR_SECONDARY_110,
|
|
COLOR_SECONDARY_120, COLOR_SECONDARY_140, COLOR_SECONDARY_150, COLOR_SECONDARY_80,
|
|
COLOR_SECONDARY_90, COLOR_TERNARY_100, COLOR_TERNARY_110, COLOR_TERNARY_120, COLOR_TERNARY_140,
|
|
COLOR_TERNARY_150, COLOR_TERNARY_80, COLOR_TERNARY_90,
|
|
};
|
|
|
|
turf::style_sheet!("src/ui/components/login.scss");
|
|
|
|
const BACKGROUND_COLORS_STR: &str = formatcp!(
|
|
"{COLOR_PRIMARY_150},{COLOR_PRIMARY_140},\
|
|
{COLOR_SECONDARY_150},{COLOR_SECONDARY_140},\
|
|
{COLOR_TERNARY_150},{COLOR_TERNARY_140}"
|
|
);
|
|
|
|
const SHAPE_1_COLORS_STR: &str = formatcp!(
|
|
"{COLOR_PRIMARY_120},{COLOR_PRIMARY_110},{COLOR_PRIMARY_100},{COLOR_PRIMARY_90},{COLOR_PRIMARY_80}"
|
|
);
|
|
|
|
const SHAPE_2_COLORS_STR: &str = formatcp!(
|
|
"{COLOR_SECONDARY_120},{COLOR_SECONDARY_110},{COLOR_SECONDARY_100},{COLOR_SECONDARY_90},{COLOR_SECONDARY_80}");
|
|
|
|
const SHAPE_3_COLORS_STR: &str = formatcp!(
|
|
"{COLOR_TERNARY_120},{COLOR_TERNARY_110},{COLOR_TERNARY_100},{COLOR_TERNARY_90},{COLOR_TERNARY_80}");
|
|
|
|
const REQUIRED_ERROR_NAME: &str = "required";
|
|
const REQUIRED_ERROR_HELPER_TEXT: &str = "This field must be completed";
|
|
|
|
const INVALID_URL_ERROR_NAME: &str = "url";
|
|
const INVALID_URL_ERROR_HELPER_TEXT: &str = "This field must be a valid URL";
|
|
|
|
const INVALID_EMAIL_ERROR_NAME: &str = "email";
|
|
const INVALID_EMAIL_ERROR_HELPER_TEXT: &str = "This field must be a valid email";
|
|
|
|
const TOO_WEAK_PASSWORD_ERROR_NAME: &str = "too_weak_password";
|
|
const TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT: &str = "The password is too weak";
|
|
|
|
const FIELDS_MISMATCH_ERROR_NAME: &str = "mismatch";
|
|
|
|
const HOMESERVER_FIELD_NAME: &str = "homeserver_url";
|
|
const ID_FIELD_NAME: &str = "id";
|
|
const PASSWORD_FIELD_NAME: &str = "password";
|
|
const CONFIRM_PASSWORD_FIELD_NAME: &str = "confirm password";
|
|
|
|
const LOGIN_ID_PLACEHOLDER: &str = "Username";
|
|
const REGISTER_ID_PLACEHOLDER: &str = "Email";
|
|
|
|
#[derive(PartialEq)]
|
|
enum Process {
|
|
Login,
|
|
Registration,
|
|
}
|
|
|
|
trait OnValidationError {
|
|
fn on_validation_error(&mut self, error: &ValidationError) {
|
|
let code = error.code.to_string();
|
|
let msg = match code.as_str() {
|
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
|
_ => None,
|
|
};
|
|
if let Some(msg) = msg {
|
|
self.invalidate(msg.to_string());
|
|
}
|
|
}
|
|
fn reset(&mut self);
|
|
fn invalidate(&mut self, helper_text: String);
|
|
fn box_clone(&self) -> Box<dyn OnValidationError>;
|
|
}
|
|
|
|
impl Clone for Box<dyn OnValidationError> {
|
|
fn clone(&self) -> Self {
|
|
self.box_clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct UrlInputHandler {
|
|
state: Signal<TextInputState>,
|
|
}
|
|
|
|
impl UrlInputHandler {
|
|
pub fn new(state: Signal<TextInputState>) -> Self {
|
|
Self { state }
|
|
}
|
|
}
|
|
|
|
impl OnValidationError for UrlInputHandler {
|
|
fn on_validation_error(&mut self, error: &ValidationError) {
|
|
let code = error.code.to_string();
|
|
let msg = match code.as_str() {
|
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
|
INVALID_URL_ERROR_NAME => Some(INVALID_URL_ERROR_HELPER_TEXT),
|
|
_ => None,
|
|
};
|
|
if let Some(msg) = msg {
|
|
self.invalidate(msg.to_string());
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.state.write().reset();
|
|
}
|
|
|
|
fn invalidate(&mut self, helper_text: String) {
|
|
self.state.write().invalidate(helper_text);
|
|
}
|
|
|
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
|
Box::new(self.clone())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct EmailInputHandler {
|
|
state: Signal<TextInputState>,
|
|
}
|
|
|
|
impl EmailInputHandler {
|
|
pub fn new(state: Signal<TextInputState>) -> Self {
|
|
Self { state }
|
|
}
|
|
}
|
|
|
|
impl OnValidationError for EmailInputHandler {
|
|
fn on_validation_error(&mut self, error: &ValidationError) {
|
|
let code = error.code.to_string();
|
|
let msg = match code.as_str() {
|
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
|
INVALID_EMAIL_ERROR_NAME => Some(INVALID_EMAIL_ERROR_HELPER_TEXT),
|
|
_ => None,
|
|
};
|
|
if let Some(msg) = msg {
|
|
self.invalidate(msg.to_string());
|
|
}
|
|
}
|
|
fn reset(&mut self) {
|
|
self.state.write().reset();
|
|
}
|
|
|
|
fn invalidate(&mut self, helper_text: String) {
|
|
self.state.write().invalidate(helper_text);
|
|
}
|
|
|
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
|
Box::new(self.clone())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct PasswordInputHandler {
|
|
state: Signal<PasswordInputState>,
|
|
}
|
|
|
|
impl PasswordInputHandler {
|
|
pub fn new(state: Signal<PasswordInputState>) -> Self {
|
|
Self { state }
|
|
}
|
|
}
|
|
|
|
impl OnValidationError for PasswordInputHandler {
|
|
fn on_validation_error(&mut self, error: &ValidationError) {
|
|
let code = error.code.to_string();
|
|
let msg = match code.as_str() {
|
|
REQUIRED_ERROR_NAME => Some(REQUIRED_ERROR_HELPER_TEXT),
|
|
INVALID_EMAIL_ERROR_NAME => Some(INVALID_EMAIL_ERROR_HELPER_TEXT),
|
|
TOO_WEAK_PASSWORD_ERROR_NAME => {
|
|
let mut score = 0.0;
|
|
if let Some(guesses_log10) = error.params.get("guesses_log10") {
|
|
if let Some(guesses_log10) = guesses_log10.as_f64() {
|
|
score = guesses_log10;
|
|
}
|
|
}
|
|
self.state.write().score = score;
|
|
Some(TOO_WEAK_PASSWORD_ERROR_HELPER_TEXT)
|
|
}
|
|
_ => None,
|
|
};
|
|
if let Some(msg) = msg {
|
|
self.invalidate(msg.to_string());
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.state.write().reset();
|
|
}
|
|
|
|
fn invalidate(&mut self, helper_text: String) {
|
|
self.state.write().invalidate(helper_text);
|
|
}
|
|
|
|
fn box_clone(&self) -> Box<dyn OnValidationError> {
|
|
Box::new(self.clone())
|
|
}
|
|
}
|
|
|
|
fn on_validation_errors(
|
|
field_errors: &HashMap<&str, &Vec<ValidationError>>,
|
|
handlers: &InputHandlers,
|
|
) {
|
|
for (field_name, errors) in field_errors {
|
|
if let Some(handler) = handlers.get(field_name) {
|
|
errors
|
|
.iter()
|
|
.for_each(|e| handler.borrow_mut().on_validation_error(e));
|
|
} else if *field_name == "__all__" {
|
|
for error in *errors {
|
|
let code = error.code.to_string();
|
|
match code.as_str() {
|
|
FIELDS_MISMATCH_ERROR_NAME => {
|
|
let values = error.params.values();
|
|
|
|
let field_names = values
|
|
.into_iter()
|
|
.filter_map(|v| v.as_str())
|
|
.collect::<Vec<_>>();
|
|
|
|
for field_name in field_names.iter() {
|
|
if let Some(handler) = handlers.get(field_name) {
|
|
let other_field_names = field_names
|
|
.iter()
|
|
.filter(|f| **f != *field_name)
|
|
.copied()
|
|
.collect::<Vec<_>>();
|
|
let other_field_names_len = other_field_names.len();
|
|
|
|
let formatted = format!(
|
|
"This field must match with {}{}{} field{}",
|
|
other_field_names[0..other_field_names_len - 1].join(", "),
|
|
if other_field_names_len > 1 {
|
|
" and "
|
|
} else {
|
|
""
|
|
},
|
|
other_field_names[other_field_names_len - 1],
|
|
if other_field_names_len > 1 { "s" } else { "" },
|
|
);
|
|
|
|
handler.borrow_mut().invalidate(formatted);
|
|
}
|
|
}
|
|
}
|
|
REQUIRED_ERROR_NAME => {
|
|
let Some(field_value) = error.params.get("field_name") else {
|
|
continue;
|
|
};
|
|
let Some(field_name) = field_value.as_str() else {
|
|
continue;
|
|
};
|
|
if let Some(handler) = handlers.get(field_name) {
|
|
handler.borrow_mut().on_validation_error(error);
|
|
}
|
|
}
|
|
other => todo!("{:?}", other),
|
|
}
|
|
}
|
|
} else {
|
|
error!("No validation state found for \"{field_name}\" field name");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Validate)]
|
|
#[validate(context = "Process")]
|
|
#[validate(schema(
|
|
function = validate_data,
|
|
use_context,
|
|
skip_on_field_errors = false
|
|
))]
|
|
struct Data {
|
|
#[validate(required, url)]
|
|
homeserver_url: Option<String>,
|
|
#[validate(required, custom(function = validate_id, use_context))]
|
|
id: Option<String>,
|
|
#[validate(required, custom(function = validate_password, use_context))]
|
|
password: Option<String>,
|
|
confirm_password: Option<String>,
|
|
}
|
|
|
|
impl Data {
|
|
fn new() -> Self {
|
|
Self {
|
|
homeserver_url: None,
|
|
id: None,
|
|
password: None,
|
|
confirm_password: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_data(data: &Data, process: &Process) -> Result<(), ValidationError> {
|
|
match process {
|
|
Process::Registration => {
|
|
let mut is_confirm_password_empty = true;
|
|
if let Some(confirm_password) = &data.confirm_password {
|
|
if !confirm_password.is_empty() {
|
|
is_confirm_password_empty = false;
|
|
}
|
|
}
|
|
if is_confirm_password_empty {
|
|
let mut err = ValidationError::new(REQUIRED_ERROR_NAME);
|
|
err.add_param(Cow::from("field_name"), &CONFIRM_PASSWORD_FIELD_NAME);
|
|
return Err(err);
|
|
}
|
|
|
|
if data.password != data.confirm_password {
|
|
let mut err = ValidationError::new(FIELDS_MISMATCH_ERROR_NAME);
|
|
err.add_param(Cow::from("field_name_1"), &PASSWORD_FIELD_NAME);
|
|
err.add_param(Cow::from("field_name_2"), &CONFIRM_PASSWORD_FIELD_NAME);
|
|
return Err(err);
|
|
}
|
|
}
|
|
Process::Login => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_id(id: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
|
if *process == Process::Registration && !id.validate_email() {
|
|
let err = ValidationError::new(INVALID_EMAIL_ERROR_NAME);
|
|
return Err(err);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct PasswordValidationResult {
|
|
score: u8,
|
|
rating: f64, // 0 <= rating <= 1
|
|
suggestions: Vec<String>,
|
|
}
|
|
impl PasswordValidationResult {
|
|
pub fn new() -> Self {
|
|
PasswordValidationResult {
|
|
score: 0,
|
|
rating: 0.0,
|
|
suggestions: Vec::<String>::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compute_password_score(
|
|
password: &str,
|
|
with_suggestions: Option<bool>,
|
|
) -> Option<PasswordValidationResult> {
|
|
let Ok(estimate) = zxcvbn(password, &[]) else {
|
|
return None;
|
|
};
|
|
|
|
let mut result = PasswordValidationResult::new();
|
|
result.score = estimate.score();
|
|
|
|
let mut rating = estimate.guesses_log10() * 4.5 / 100.0;
|
|
if rating > 1.0 {
|
|
rating = 1.0;
|
|
};
|
|
result.rating = rating;
|
|
|
|
let with_suggestions = with_suggestions.unwrap_or(false);
|
|
if with_suggestions {
|
|
if let Some(feedback) = estimate.feedback() {
|
|
for suggestion in feedback.suggestions() {
|
|
result.suggestions.push(suggestion.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(result)
|
|
}
|
|
|
|
fn validate_password(password: &Option<String>, process: &Process) -> Result<(), ValidationError> {
|
|
if *process == Process::Registration {
|
|
if let Some(password) = password {
|
|
if let Some(result) = compute_password_score(password, Some(true)) {
|
|
// TODO: To configuration?
|
|
if result.score <= 2 {
|
|
let mut err = ValidationError::new(TOO_WEAK_PASSWORD_ERROR_NAME);
|
|
err.add_param(Cow::from("score"), &result.score);
|
|
err.add_param(Cow::from("rating"), &result.rating);
|
|
err.add_param(Cow::from("suggestions"), &result.suggestions);
|
|
|
|
return Err(err);
|
|
}
|
|
} else {
|
|
error!("Unable to compute the password score");
|
|
}
|
|
} else {
|
|
warn!("Password parameter is None");
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn on_login(session: &GlobalSignal<Session>, data: Signal<Data>) -> Result<(), ValidationErrors> {
|
|
let data = data.read();
|
|
|
|
match data.validate_with_args(&Process::Login) {
|
|
Ok(_) => {
|
|
session.write().update(
|
|
data.homeserver_url.clone(),
|
|
data.id.clone(),
|
|
data.password.clone(),
|
|
);
|
|
Ok(())
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
fn on_register(
|
|
_session: &GlobalSignal<Session>,
|
|
data: Signal<Data>,
|
|
) -> Result<(), ValidationErrors> {
|
|
let data = data.read();
|
|
|
|
match data.validate_with_args(&Process::Registration) {
|
|
Ok(_) => {
|
|
error!("TODO: Manage registration process");
|
|
Ok(())
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct InputHandlers<'a> {
|
|
handlers: HashMap<&'a str, Rc<RefCell<dyn OnValidationError>>>,
|
|
}
|
|
|
|
impl<'a> InputHandlers<'a> {
|
|
fn new() -> Self {
|
|
Self {
|
|
handlers: HashMap::new(),
|
|
}
|
|
}
|
|
fn insert<T: 'static + OnValidationError>(&mut self, name: &'a str, handler: T) {
|
|
self.handlers.insert(name, Rc::new(RefCell::new(handler)));
|
|
}
|
|
fn get(&self, name: &'a str) -> Option<&Rc<RefCell<(dyn OnValidationError + 'static)>>> {
|
|
if let Some(handler) = self.handlers.get(name) {
|
|
return Some(handler);
|
|
}
|
|
None
|
|
}
|
|
fn reset_handlers(&self) {
|
|
self.handlers.values().for_each(|h| h.borrow_mut().reset());
|
|
}
|
|
}
|
|
|
|
macro_rules! on_input {
|
|
($data_ref:ident, $data_field:ident) => {
|
|
move |evt: FormEvent| {
|
|
let value = evt.value();
|
|
$data_ref.write().$data_field = if !value.is_empty() { Some(value) } else { None }
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! refresh_password_state {
|
|
($data:ident, $data_field:ident, $state:ident) => {
|
|
let mut rating = 0.0;
|
|
if let Some(password) = &$data.peek().$data_field {
|
|
if let Some(result) = compute_password_score(password, None) {
|
|
rating = result.rating;
|
|
}
|
|
}
|
|
$state.write().score = rating;
|
|
};
|
|
}
|
|
|
|
fn generate_modal(
|
|
config: &PasswordSuggestionsModalConfig,
|
|
on_confirm: impl FnMut(Event<MouseData>) + 'static,
|
|
) -> Element {
|
|
let suggestions = config.suggestions.get(PASSWORD_FIELD_NAME);
|
|
|
|
let mut rendered_suggestions = Vec::<Element>::new();
|
|
if let Some(suggestions) = suggestions {
|
|
if suggestions.len() == 1 {
|
|
rendered_suggestions.push(rsx!({ suggestions[0].as_str() }));
|
|
} else {
|
|
suggestions
|
|
.iter()
|
|
.for_each(|s| rendered_suggestions.push(rsx!(li { {s.as_str()} })));
|
|
}
|
|
}
|
|
|
|
rsx! {
|
|
Modal {
|
|
severity: config.severity,
|
|
title: config.title.as_ref(),
|
|
on_confirm: on_confirm,
|
|
|
|
div {
|
|
{rendered_suggestions.iter()}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type Suggestions<'a> = HashMap<&'a str, Vec<String>>;
|
|
|
|
#[derive(Clone)]
|
|
struct PasswordSuggestionsModalConfig<'a> {
|
|
severity: Severity,
|
|
title: String,
|
|
suggestions: Suggestions<'a>,
|
|
}
|
|
impl<'a> PasswordSuggestionsModalConfig<'a> {
|
|
fn new(severity: Option<Severity>, title: String) -> Self {
|
|
Self {
|
|
severity: severity.unwrap_or(Severity::Critical),
|
|
title,
|
|
suggestions: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn Login() -> Element {
|
|
debug!("Login rendering");
|
|
|
|
let mut data = use_signal(Data::new);
|
|
|
|
let mut current_process = use_signal(|| Process::Login);
|
|
|
|
let data_lock = data.read();
|
|
|
|
let homeserver_url = data_lock.homeserver_url.as_deref().unwrap_or("");
|
|
let homeserver_url_state = use_signal(TextInputState::new);
|
|
let id = data_lock.id.as_deref().unwrap_or("");
|
|
let id_state = use_signal(TextInputState::new);
|
|
let password = data_lock.password.as_deref().unwrap_or("");
|
|
let mut password_state = use_signal(PasswordInputState::new);
|
|
let confirm_password = data_lock.confirm_password.as_deref().unwrap_or("");
|
|
let mut confirm_password_state = use_signal(PasswordInputState::new);
|
|
|
|
let mut handlers = InputHandlers::new();
|
|
handlers.insert(
|
|
HOMESERVER_FIELD_NAME,
|
|
UrlInputHandler::new(homeserver_url_state),
|
|
);
|
|
handlers.insert(ID_FIELD_NAME, EmailInputHandler::new(id_state));
|
|
handlers.insert(
|
|
PASSWORD_FIELD_NAME,
|
|
PasswordInputHandler::new(password_state),
|
|
);
|
|
handlers.insert(
|
|
CONFIRM_PASSWORD_FIELD_NAME,
|
|
PasswordInputHandler::new(confirm_password_state),
|
|
);
|
|
|
|
let mut spinner_animated = use_signal(|| false);
|
|
let mut id_placeholder = use_signal(|| LOGIN_ID_PLACEHOLDER);
|
|
|
|
let mut random_avatar_future = use_resource(move || async move {
|
|
let shape_config = ShapeConfig::new(
|
|
BACKGROUND_COLORS_STR,
|
|
SHAPE_1_COLORS_STR,
|
|
SHAPE_2_COLORS_STR,
|
|
SHAPE_3_COLORS_STR,
|
|
);
|
|
generate_random_svg_shape(Some(&shape_config)).await
|
|
});
|
|
|
|
let avatar = (*random_avatar_future.read_unchecked())
|
|
.as_ref()
|
|
.map(|svg| {
|
|
rsx! {
|
|
div {
|
|
class: ClassName::LOGIN_AVATAR_CONTENT,
|
|
dangerous_inner_html: svg.as_str(),
|
|
}
|
|
}
|
|
});
|
|
|
|
if *spinner_animated.read() && SESSION.read().is_logged {
|
|
spinner_animated.set(false);
|
|
}
|
|
|
|
refresh_password_state!(data, password, password_state);
|
|
refresh_password_state!(data, confirm_password, confirm_password_state);
|
|
|
|
let mut modal_configs = use_signal(Vec::<PasswordSuggestionsModalConfig>::new);
|
|
let mut modal_config = use_signal(|| None::<PasswordSuggestionsModalConfig>);
|
|
|
|
if !modal_configs.read().is_empty() && modal_config.read().is_none() {
|
|
modal_config.set(modal_configs.write().pop());
|
|
}
|
|
|
|
let on_clicked_login = {
|
|
to_owned![handlers];
|
|
|
|
move |_| {
|
|
handlers.reset_handlers();
|
|
|
|
if *current_process.read() == Process::Registration {
|
|
current_process.set(Process::Login);
|
|
data.write().id = None;
|
|
return;
|
|
}
|
|
|
|
spinner_animated.set(true);
|
|
|
|
if let Err(errors) = on_login(&SESSION, data) {
|
|
let field_errors = errors.field_errors();
|
|
on_validation_errors(&field_errors, &handlers);
|
|
}
|
|
}
|
|
};
|
|
|
|
let on_clicked_register = {
|
|
to_owned![handlers, modal_configs];
|
|
|
|
move |_| {
|
|
if *current_process.read() == Process::Login {
|
|
current_process.set(Process::Registration);
|
|
data.write().id = None;
|
|
return;
|
|
}
|
|
|
|
handlers.reset_handlers();
|
|
|
|
spinner_animated.set(true);
|
|
|
|
if let Err(errors) = on_register(&SESSION, data) {
|
|
let field_name = PASSWORD_FIELD_NAME;
|
|
|
|
let field_errors = errors.field_errors();
|
|
if let Some(errors) = field_errors.get(field_name) {
|
|
let mut suggestions_msgs = Vec::<String>::new();
|
|
errors
|
|
.iter()
|
|
.filter(|e| e.code == TOO_WEAK_PASSWORD_ERROR_NAME)
|
|
.for_each(|e| {
|
|
if let Some(suggestions_value) = e.params.get("suggestions") {
|
|
if let Some(suggestion) = suggestions_value.as_array() {
|
|
let msgs = suggestion.iter().map(|msg| {
|
|
let msg = msg.to_string();
|
|
let without_quote_marks = &msg[1..msg.len() - 1];
|
|
without_quote_marks.to_string()
|
|
});
|
|
suggestions_msgs.extend(msgs);
|
|
}
|
|
}
|
|
});
|
|
|
|
if !suggestions_msgs.is_empty() {
|
|
let mut modal_config = PasswordSuggestionsModalConfig::new(
|
|
None,
|
|
"Let's talk about your password".to_string(),
|
|
);
|
|
modal_config
|
|
.suggestions
|
|
.insert(field_name, suggestions_msgs);
|
|
|
|
modal_configs.write().push(modal_config);
|
|
}
|
|
}
|
|
|
|
on_validation_errors(&field_errors, &handlers);
|
|
}
|
|
|
|
spinner_animated.set(false);
|
|
}
|
|
};
|
|
|
|
let mut classes: [&str; 2] = [ClassName::LOGIN, ""];
|
|
let mut password_classes: [&str; 2] = [ClassName::LOGIN_PASSWORD, ""];
|
|
let mut confirm_password_classes: [&str; 2] = [ClassName::LOGIN_CONFIRM_PASSWORD, ""];
|
|
|
|
match *current_process.read() {
|
|
Process::Registration => {
|
|
classes[1] = ClassName::REGISTER;
|
|
password_classes[1] = ClassName::SHOW;
|
|
confirm_password_classes[1] = ClassName::SHOW;
|
|
|
|
if *id_placeholder.read() != REGISTER_ID_PLACEHOLDER {
|
|
id_placeholder.set(REGISTER_ID_PLACEHOLDER);
|
|
}
|
|
}
|
|
Process::Login => {
|
|
if *id_placeholder.read() != LOGIN_ID_PLACEHOLDER {
|
|
id_placeholder.set(LOGIN_ID_PLACEHOLDER);
|
|
}
|
|
}
|
|
}
|
|
|
|
let on_modal_confirm = move |_: Event<MouseData>| {
|
|
modal_config.set(None);
|
|
};
|
|
|
|
let rendered_modal = modal_config
|
|
.read()
|
|
.as_ref()
|
|
.map(|modal_config| rsx!({ generate_modal(modal_config, on_modal_confirm) }));
|
|
|
|
let classes_str = classes.join(" ");
|
|
let password_classes_str = password_classes.join(" ");
|
|
let confirm_password_classes_str = confirm_password_classes.join(" ");
|
|
|
|
rsx! {
|
|
style { {STYLE_SHEET} }
|
|
|
|
div {
|
|
class: "{classes_str}",
|
|
|
|
div {
|
|
class: ClassName::LOGIN_AVATAR,
|
|
|
|
onclick: move |_| {
|
|
random_avatar_future.restart()
|
|
},
|
|
|
|
{avatar}
|
|
}
|
|
|
|
div {
|
|
class: ClassName::LOGIN_HOMESERVER,
|
|
|
|
TextInput {
|
|
placeholder: "Homeserver URL",
|
|
value: "{homeserver_url}",
|
|
state: homeserver_url_state,
|
|
oninput: on_input![data, homeserver_url],
|
|
}
|
|
}
|
|
|
|
div {
|
|
class: ClassName::LOGIN_ID,
|
|
|
|
TextInput {
|
|
placeholder: "{id_placeholder}",
|
|
value: "{id}",
|
|
state: id_state,
|
|
oninput: on_input![data, id],
|
|
}
|
|
}
|
|
|
|
div {
|
|
class: "{password_classes_str}",
|
|
|
|
PasswordTextInput {
|
|
placeholder: "Password",
|
|
value: "{password}",
|
|
state: password_state,
|
|
oninput: on_input![data, password],
|
|
}
|
|
}
|
|
|
|
div {
|
|
class: "{confirm_password_classes_str}",
|
|
|
|
PasswordTextInput {
|
|
placeholder: "Confirm Password",
|
|
value: "{confirm_password}",
|
|
state: confirm_password_state,
|
|
oninput: on_input![data, confirm_password],
|
|
}
|
|
}
|
|
|
|
div {
|
|
class: ClassName::LOGIN_SPINNER,
|
|
|
|
Spinner {
|
|
animate: *spinner_animated.read(),
|
|
}
|
|
}
|
|
|
|
div {
|
|
class: ClassName::LOGIN_REGISTER_BUTTON,
|
|
|
|
RegisterButton {
|
|
onclick: on_clicked_register,
|
|
}
|
|
}
|
|
|
|
div {
|
|
class: ClassName::LOGIN_LOGIN_BUTTON,
|
|
|
|
LoginButton {
|
|
focus: true,
|
|
onclick: on_clicked_login,
|
|
}
|
|
}
|
|
}
|
|
|
|
{rendered_modal}
|
|
}
|
|
}
|