synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;

/// Password reset form component.
#[component]
pub fn PasswordResetForm() -> Element {
    let mut homeserver = use_signal(|| "https://matrix.org".to_string());
    let mut email = use_signal(|| String::new());
    let mut error_message = use_signal(|| Option::<String>::None);
    let mut success = use_signal(|| false);
    let mut is_loading = use_signal(|| false);

    let on_submit = move |evt: Event<FormData>| {
        evt.prevent_default();

        let hs = homeserver.read().clone();
        let email_val = email.read().clone();
        if email_val.is_empty() {
            error_message.set(Some("Please enter your email address".to_string()));
            return;
        }

        is_loading.set(true);
        error_message.set(None);

        spawn(async move {
            match request_password_reset(&hs, &email_val).await {
                Ok(()) => {
                    success.set(true);
                }
                Err(e) => {
                    error_message.set(Some(e));
                }
            }
            is_loading.set(false);
        });
    };

    if *success.read() {
        return rsx! {
            div {
                class: "password-reset__success",
                h3 { "Check your email" }
                p { "We've sent a password reset link to your email address." }
                p { "Follow the instructions in the email to reset your password, then return here to sign in." }
            }
        };
    }

    rsx! {
        form {
            class: "password-reset-form",
            onsubmit: on_submit,

            if let Some(ref err) = *error_message.read() {
                div {
                    class: "password-reset-form__error",
                    "{err}"
                }
            }

            p {
                class: "password-reset-form__description",
                "Enter the email address associated with your account and we'll send you a link to reset your password."
            }

            div {
                class: "password-reset-form__field",
                label { r#for: "reset-homeserver", "Homeserver" }
                input {
                    id: "reset-homeserver",
                    r#type: "url",
                    placeholder: "https://matrix.org",
                    value: "{homeserver}",
                    oninput: move |evt| homeserver.set(evt.value()),
                    disabled: *is_loading.read(),
                }
            }

            div {
                class: "password-reset-form__field",
                label { r#for: "reset-email", "Email" }
                input {
                    id: "reset-email",
                    r#type: "email",
                    placeholder: "your@email.com",
                    value: "{email}",
                    oninput: move |evt| email.set(evt.value()),
                    disabled: *is_loading.read(),
                }
            }

            button {
                r#type: "submit",
                class: "password-reset-form__submit",
                disabled: *is_loading.read(),
                if *is_loading.read() {
                    "Sending..."
                } else {
                    "Reset Password"
                }
            }
        }
    }
}

/// Request a password reset email via the Matrix API.
async fn request_password_reset(homeserver: &str, email: &str) -> Result<(), String> {
    let client = crate::client::build_client(homeserver)
        .await
        .map_err(|e| format!("Failed to connect to homeserver: {e}"))?;

    use matrix_sdk::ruma::api::client::account::request_password_change_token_via_email;

    let secret_str = uuid::Uuid::new_v4().to_string();
    let client_secret: matrix_sdk::ruma::OwnedClientSecret = secret_str.try_into()
        .map_err(|_| "Failed to create client secret".to_string())?;

    let request = request_password_change_token_via_email::v3::Request::new(
        client_secret,
        email.to_owned(),
        1u32.into(),
    );

    match client.send(request).await {
        Ok(_response) => {
            tracing::info!("Password reset email sent to {email}");
            Ok(())
        }
        Err(e) => {
            let err_str = e.to_string();
            if err_str.contains("M_THREEPID_NOT_FOUND") {
                Err("No account found with that email address".to_string())
            } else if err_str.contains("M_SERVER_NOT_TRUSTED") {
                Err("The identity server is not trusted by this homeserver".to_string())
            } else {
                Err(format!("Failed to send reset email: {e}"))
            }
        }
    }
}