Files
PinePods-nix/PinePods-0.8.2/web/src/components/setting_components/mfa_settings.rs
2026-03-03 10:57:43 -05:00

363 lines
18 KiB
Rust

use crate::components::context::AppState;
use crate::components::gen_funcs::format_error_message;
use crate::components::safehtml::SafeHtml;
use crate::requests::setting_reqs::{
call_disable_mfa, call_generate_mfa_secret, call_mfa_settings, call_verify_temp_mfa,
};
use std::borrow::Borrow;
use wasm_bindgen::JsCast;
use yew::platform::spawn_local;
use yew::prelude::*;
use yewdux::prelude::*;
use i18nrs::yew::use_translation;
#[function_component(MFAOptions)]
pub fn mfa_options() -> Html {
let (i18n, _) = use_translation();
let (state, _dispatch) = use_store::<AppState>();
let api_key = state.auth_details.as_ref().map(|ud| ud.api_key.clone());
let user_id = state.user_details.as_ref().map(|ud| ud.UserID.clone());
let server_name = state.auth_details.as_ref().map(|ud| ud.server_name.clone());
let _error_message = state.error_message.clone();
let mfa_status = use_state(|| false);
let code = use_state(|| "".to_string());
// Capture i18n strings before they get moved
let i18n_error_getting_mfa_status = i18n.t("mfa_settings.error_getting_mfa_status").to_string();
let i18n_mfa_code_verification_failed = i18n.t("mfa_settings.mfa_code_verification_failed").to_string();
let i18n_failed_to_verify_mfa_code = i18n.t("mfa_settings.failed_to_verify_mfa_code").to_string();
let i18n_error_disabling_mfa = i18n.t("mfa_settings.error_disabling_mfa").to_string();
let i18n_failed_to_generate_totp_secret = i18n.t("mfa_settings.failed_to_generate_totp_secret").to_string();
let i18n_close_modal = i18n.t("common.close_modal").to_string();
let i18n_setup_mfa = i18n.t("mfa_settings.setup_mfa").to_string();
let i18n_scan_qr_or_enter_code = i18n.t("mfa_settings.scan_qr_or_enter_code").to_string();
let i18n_verify_code = i18n.t("mfa_settings.verify_code").to_string();
let i18n_verify = i18n.t("mfa_settings.verify").to_string();
let i18n_close = i18n.t("common.cancel").to_string();
let i18n_mfa_options = i18n.t("mfa_settings.mfa_options").to_string();
let i18n_mfa_description = i18n.t("mfa_settings.mfa_description").to_string();
let i18n_enable_mfa = i18n.t("mfa_settings.enable_mfa").to_string();
let effect_user_id = user_id.clone();
let effect_api_key = api_key.clone();
let effect_server_name = server_name.clone();
let dispatch_effect = _dispatch.clone();
{
let mfa_status = mfa_status.clone();
let i18n_error_getting_mfa_status_clone1 = i18n_error_getting_mfa_status.clone();
use_effect_with(
(effect_api_key.clone(), effect_server_name.clone()),
move |(_api_key, _server_name)| {
let mfa_status = mfa_status.clone();
let api_key = effect_api_key.clone();
let server_name = effect_server_name.clone();
let user_id = effect_user_id.clone();
let i18n_error_getting_mfa_status = i18n_error_getting_mfa_status_clone1.clone();
let future = async move {
if let (Some(api_key), Some(server_name)) = (api_key, server_name) {
let response =
call_mfa_settings(server_name, api_key.unwrap(), user_id.unwrap())
.await;
match response {
Ok(mfa_settings_response) => {
mfa_status.set(mfa_settings_response);
}
Err(e) => {
let formatted_error = format_error_message(&e.to_string());
dispatch_effect.reduce_mut(|audio_state| {
audio_state.error_message = Option::from(format!(
"{}{}",
i18n_error_getting_mfa_status.clone(),
formatted_error
))
});
}
}
}
};
spawn_local(future);
// Return cleanup function
|| {}
},
);
}
let dispatch_refresh = _dispatch.clone();
// Re-fetch MFA status after setup is complete
{
let mfa_status = mfa_status.clone();
let api_key = api_key.clone();
let server_name = server_name.clone();
let user_id = user_id.clone();
let i18n_error_getting_mfa_status_clone2 = i18n_error_getting_mfa_status.clone();
use_effect_with(mfa_status.clone(), move |_| {
let mfa_status = mfa_status.clone();
let api_key = api_key.clone();
let server_name = server_name.clone();
let user_id = user_id.clone();
let i18n_error_getting_mfa_status = i18n_error_getting_mfa_status_clone2.clone();
wasm_bindgen_futures::spawn_local(async move {
if let (Some(api_key), Some(server_name)) = (api_key, server_name) {
match call_mfa_settings(server_name, api_key.unwrap(), user_id.unwrap()).await {
Ok(mfa_settings_response) => {
mfa_status.set(mfa_settings_response);
}
Err(e) => {
let formatted_error = format_error_message(&e.to_string());
dispatch_refresh.reduce_mut(|audio_state| {
audio_state.error_message = Option::from(format!(
"{}{}",
i18n_error_getting_mfa_status.clone(),
formatted_error
))
});
}
}
}
});
|| ()
});
}
// let html_self_service = self_service_status.clone();
let loading = use_state(|| false);
// Define the state of the application
#[derive(Clone, PartialEq)]
enum PageState {
Hidden,
Setup,
}
// Define the initial state
let page_state = use_state(|| PageState::Hidden);
let mfa_code = use_state(|| String::new());
let mfa_secret = use_state(|| String::new());
// Define the function to close the modal
let close_modal = {
let page_state = page_state.clone();
Callback::from(move |_| {
page_state.set(PageState::Hidden);
})
};
let on_background_click = {
let on_close_modal = close_modal.clone();
Callback::from(move |e: MouseEvent| {
let target = e.target().unwrap();
let element = target.dyn_into::<web_sys::Element>().unwrap();
if element.tag_name() == "DIV" {
on_close_modal.emit(e);
}
})
};
let stop_propagation = Callback::from(|e: MouseEvent| {
e.stop_propagation();
});
let open_setup_modal = {
let mfa_code = mfa_code.clone();
let page_state = page_state.clone();
let mfa_secret = mfa_secret.clone();
let server_name = server_name.clone(); // Replace with actual server name
let api_key = api_key.clone(); // Replace with actual API key
let user_id = user_id.clone(); // Replace with actual user ID
let mfa_status = mfa_status.clone();
let i18n_error_disabling_mfa = i18n_error_disabling_mfa.clone();
let i18n_failed_to_generate_totp_secret = i18n_failed_to_generate_totp_secret.clone();
Callback::from(move |_| {
let i18n_error_disabling_mfa = i18n_error_disabling_mfa.clone();
let i18n_failed_to_generate_totp_secret = i18n_failed_to_generate_totp_secret.clone();
let mfa_code = mfa_code.clone();
let page_state = page_state.clone();
let mfa_secret = mfa_secret.clone();
let server_name = server_name.clone();
let api_key = api_key.clone();
let user_id = user_id;
let mfa_status = mfa_status.clone();
// Now call the API to generate the TOTP secret
wasm_bindgen_futures::spawn_local(async move {
match call_generate_mfa_secret(
server_name.clone().unwrap(),
api_key.clone().unwrap().unwrap(),
user_id.clone().unwrap(),
)
.await
{
Ok(response) => {
if *mfa_status {
let result = call_disable_mfa(
&server_name.unwrap(),
&api_key.unwrap().unwrap(),
user_id.unwrap(),
)
.await;
match result {
Ok(_) => {
// Handle success
page_state.set(PageState::Hidden); // Hide the modal
mfa_status.set(false);
}
Err(e) => {
// Handle error
log::error!("{}{:?}", i18n_error_disabling_mfa, e);
}
}
} else {
mfa_secret.set(response.secret);
mfa_code.set(response.qr_code_svg); // Directly use the SVG QR code
page_state.set(PageState::Setup); // Move to the setup page state
}
}
Err(e) => {
log::error!("{}{}", i18n_failed_to_generate_totp_secret, e);
// Handle error appropriately
}
}
});
})
};
// Define the function to close the modal
let verify_code = {
let page_state = page_state.clone();
let api_key = api_key.clone();
let user_id = state.user_details.as_ref().map(|ud| ud.UserID.clone());
let server_name = server_name.clone();
let code = code.clone();
let mfa_status_clone = mfa_status.clone();
let i18n_mfa_code_verification_failed = i18n_mfa_code_verification_failed.clone();
let i18n_failed_to_verify_mfa_code = i18n_failed_to_verify_mfa_code.clone();
Callback::from(move |_| {
let i18n_mfa_code_verification_failed = i18n_mfa_code_verification_failed.clone();
let i18n_failed_to_verify_mfa_code = i18n_failed_to_verify_mfa_code.clone();
let api_key = api_key.clone();
let user_id = user_id.clone();
let server_name = server_name.clone();
let page_state = page_state.clone();
let code = code.clone();
let _dispatch = _dispatch.clone();
let mfa_status_update = mfa_status_clone.clone();
wasm_bindgen_futures::spawn_local(async move {
match call_verify_temp_mfa(
&server_name.unwrap(),
&api_key.unwrap().unwrap(),
user_id.unwrap(),
(*code).clone(),
)
.await
{
Ok(response) => {
if response.verified {
// Handle successful verification, e.g., updating UI state or navigating
page_state.set(PageState::Hidden); // Example: hiding MFA prompt
mfa_status_update.set(true); // Update MFA status
// refresh_mfa_status.emit(());
} else {
_dispatch.reduce_mut(|audio_state| {
audio_state.error_message =
Option::from(i18n_mfa_code_verification_failed.clone())
});
// Handle failed verification, e.g., showing an error message
}
}
Err(e) => {
let formatted_error = format_error_message(&e.to_string());
_dispatch.reduce_mut(|audio_state| {
audio_state.error_message = Option::from(format!(
"{}{}",
i18n_failed_to_verify_mfa_code,
formatted_error
))
});
// Handle error appropriately, e.g., showing an error message
}
}
});
})
};
let on_code_change = {
let code = code.clone();
Callback::from(move |e: InputEvent| {
code.set(
e.target_unchecked_into::<web_sys::HtmlInputElement>()
.value(),
);
})
};
// let svg_data_url = format!("data:image/svg+xml;utf8,{}", url_encode(&(*mfa_code).clone()));
let qr_code_svg = (*mfa_code).clone();
let setup_mfa_modal = html! {
<div id="setup-mfa-modal" tabindex="-1" aria-hidden="true" class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-[calc(100%-1rem)] max-h-full bg-black bg-opacity-25" onclick={on_background_click.clone()}>
<div class="modal-container relative p-4 w-full max-w-md max-h-full rounded-lg shadow" onclick={stop_propagation.clone()}>
<div class="modal-container relative rounded-lg shadow">
<div class="flex flex-col items-start justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<button onclick={close_modal.clone()} class="self-end text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
</svg>
<span class="sr-only">{&i18n_close_modal}</span>
</button>
<h3 class="text-xl font-semibold">
{&i18n_setup_mfa}
</h3>
<p class="item_container-text text-m font-semibold">
{&i18n_scan_qr_or_enter_code}
</p>
<div class="mt-4 self-center bg-white rounded-lg overflow-hidden p-4 shadow-lg">
<SafeHtml html={qr_code_svg} />
</div>
// More HTML as needed
<div class="mfa-code-box mt-4 p-4 rounded-md overflow-x-auto whitespace-nowrap max-w-full">
{(*mfa_secret).clone()}
</div>
<div>
<label for="fullname" class="block mb-2 mt-2 text-sm font-semibold font-medium">{&i18n_verify_code}</label>
<input oninput={on_code_change} type="text" id="fullname" name="fullname" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required=true />
</div>
<div class="flex justify-between space-x-4">
<button onclick={verify_code.clone()} class="mt-4 download-button font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
{&i18n_verify}
</button>
<button onclick={close_modal.clone()} class="mt-4 download-button font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
{&i18n_close}
</button>
</div>
</div>
</div>
</div>
</div>
};
html! {
<>
{
match *page_state {
PageState::Setup => setup_mfa_modal,
_ => html! {},
}
}
<div class="p-4"> // You can adjust the padding as needed
<p class="item_container-text text-lg font-bold mb-4">{&i18n_mfa_options}</p>
<p class="item_container-text text-md mb-4">{&i18n_mfa_description}</p> // Styled paragraph
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" disabled={**loading.borrow()} checked={**mfa_status.borrow()} class="sr-only peer" onclick={open_setup_modal.clone()} />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span class="ms-3 text-sm font-medium item_container-text">{&i18n_enable_mfa}</span>
</label>
</div>
</>
}
}