use crate::components::context::AppState; use crate::components::gen_funcs::format_error_message; use crate::requests::setting_reqs::{ call_add_oidc_provider, call_list_oidc_providers, call_remove_oidc_provider, call_update_oidc_provider, AddOIDCProviderRequest, OIDCProvider, }; use gloo_events::EventListener; use i18nrs::yew::use_translation; use wasm_bindgen::JsCast; use web_sys::HtmlElement; use web_sys::HtmlInputElement; use yew::prelude::*; use yewdux::prelude::*; #[derive(Clone, PartialEq)] enum PageState { Hidden, AddProvider, EditProvider(i32), // provider_id } #[derive(Clone, PartialEq, Debug)] pub struct ScopeOption { pub id: String, pub name: String, pub description: String, pub value: String, pub provider_type: ProviderType, } #[derive(Clone, PartialEq, Debug, Eq)] pub enum ProviderType { Standard, GitHub, Google, Microsoft, } // Create a helper function to get scope collections // This should be defined at the module level, outside of any component fn get_scope_collections() -> (Vec, Vec) { // Standard OIDC scopes let standard_scopes = vec![ ScopeOption { id: "openid".to_string(), name: "OpenID".to_string(), description: "Provides verifiable identity".to_string(), value: "openid".to_string(), provider_type: ProviderType::Standard, }, ScopeOption { id: "email".to_string(), name: "Email".to_string(), description: "Access to email address".to_string(), value: "email".to_string(), provider_type: ProviderType::Standard, }, ScopeOption { id: "profile".to_string(), name: "Profile".to_string(), description: "Basic profile info like name".to_string(), value: "profile".to_string(), provider_type: ProviderType::Standard, }, ]; // GitHub-specific scopes let github_scopes = vec![ ScopeOption { id: "read_user".to_string(), name: "Read User".to_string(), description: "Read user profile data".to_string(), value: "read:user".to_string(), provider_type: ProviderType::GitHub, }, ScopeOption { id: "user_email".to_string(), name: "User Email".to_string(), description: "Access to email address(es)".to_string(), value: "user:email".to_string(), provider_type: ProviderType::GitHub, }, ]; (standard_scopes, github_scopes) } // Helper function to format scopes for the request - also at module level pub fn format_scopes_for_request(scopes: &[String], provider_type: &ProviderType) -> String { let (standard_scopes, github_scopes) = get_scope_collections(); if scopes.is_empty() { // Provide defaults based on provider match provider_type { ProviderType::GitHub => "read:user,user:email".to_string(), _ => "openid email profile".to_string(), } } else { // Map selected scope IDs to their values let scope_values: Vec = scopes .iter() .filter_map(|id| { // Find the matching scope option - use a function to get the right iterator let mut iter = match provider_type { ProviderType::GitHub => github_scopes.iter().chain(standard_scopes.iter()), _ => standard_scopes.iter().chain([].iter()), // Chain with empty iterator for the default case }; iter.find(|s| &s.id == id).map(|s| s.value.clone()) }) .collect(); scope_values.join(" ") } } #[derive(Properties, PartialEq)] pub struct ScopeSelectorProps { pub selected_scopes: Vec, pub on_select: Callback>, pub auth_url: String, pub token_url: String, pub user_info_url: String, } #[function_component(ScopeSelector)] pub fn scope_selector(props: &ScopeSelectorProps) -> Html { let is_open = use_state(|| false); let dropdown_ref = use_node_ref(); let (standard_scopes, github_scopes) = get_scope_collections(); // Combine all scopes based on the detected provider let detected_provider = detect_provider(&props.auth_url, &props.token_url, &props.user_info_url); let available_scopes = match detected_provider { ProviderType::GitHub => [&standard_scopes[..], &github_scopes[..]].concat(), _ => standard_scopes, }; // Handle clicking outside to close dropdown { let is_open = is_open.clone(); let dropdown_ref = dropdown_ref.clone(); use_effect_with(dropdown_ref.clone(), move |dropdown_ref| { let document = web_sys::window().unwrap().document().unwrap(); let dropdown_element = dropdown_ref.cast::(); let listener = EventListener::new(&document, "click", move |event| { if let Some(target) = event.target() { if let Some(dropdown) = &dropdown_element { if let Ok(node) = target.dyn_into::() { if !dropdown.contains(Some(&node)) { is_open.set(false); } } } } }); || drop(listener) }); } let toggle_dropdown = { let is_open = is_open.clone(); Callback::from(move |e: MouseEvent| { e.stop_propagation(); is_open.set(!*is_open); }) }; let toggle_scope_selection = { let selected = props.selected_scopes.clone(); let on_select = props.on_select.clone(); Callback::from(move |scope_id: String| { let mut new_selection = selected.clone(); if let Some(pos) = new_selection.iter().position(|id| id == &scope_id) { new_selection.remove(pos); } else { new_selection.push(scope_id); } on_select.emit(new_selection); }) }; // Format selected scopes for display let formatted_scopes = if props.selected_scopes.is_empty() { if detected_provider == ProviderType::GitHub { "GitHub: read:user,user:email".to_string() } else if detected_provider == ProviderType::Google { "OpenID Connect: openid email profile".to_string() } else { "OpenID Connect: openid email profile".to_string() } } else { let selected_names: Vec = available_scopes .iter() .filter(|scope| props.selected_scopes.contains(&scope.id)) .map(|scope| scope.name.clone()) .collect(); selected_names.join(", ") }; html! {
{ if *is_open { let standard_group = available_scopes.iter() .filter(|s| s.provider_type == ProviderType::Standard) .collect::>(); let github_group = available_scopes.iter() .filter(|s| s.provider_type == ProviderType::GitHub) .collect::>(); let google_group = available_scopes.iter() .filter(|s| s.provider_type == ProviderType::Google) .collect::>(); html! { } } else { html! {} } }
} } // Helper function to render a group of scopes fn render_scope_group( scopes: Vec<&ScopeOption>, selected_scopes: &[String], toggle_callback: &Callback, ) -> Html { scopes .iter() .map(|scope| { let is_selected = selected_scopes.contains(&scope.id); let onclick = { let toggle = toggle_callback.clone(); let id = scope.id.clone(); Callback::from(move |_| toggle.emit(id.clone())) }; html! {

{&scope.description}

{&scope.value}
if is_selected { }
} }) .collect::() } // Helper function to detect provider type based on URLs fn detect_provider(auth_url: &str, token_url: &str, user_info_url: &str) -> ProviderType { let urls = [auth_url, token_url, user_info_url]; for url in urls { if url.contains("github.com") { return ProviderType::GitHub; } else if url.contains("google") || url.contains("googleapis.com") { return ProviderType::Google; } else if url.contains("microsoft") || url.contains("azure") || url.contains("microsoftonline") { return ProviderType::Microsoft; } } ProviderType::Standard } #[function_component(OIDCSettings)] pub fn oidc_settings() -> Html { let (i18n, _) = use_translation(); let (state, _dispatch) = use_store::(); let page_state = use_state(|| PageState::Hidden); let providers = use_state(|| Vec::::new()); let update_trigger = use_state(|| false); // Capture i18n strings before they get moved let i18n_failed_to_fetch_oidc_providers = i18n.t("oidc.failed_to_fetch_oidc_providers").to_string(); let i18n_provider_successfully_removed = i18n.t("oidc.provider_successfully_removed").to_string(); let i18n_failed_to_remove_provider = i18n.t("oidc.failed_to_remove_provider").to_string(); let i18n_oidc_provider_successfully_added = i18n.t("oidc.oidc_provider_successfully_added").to_string(); let i18n_failed_to_add_provider = i18n.t("oidc.failed_to_add_provider").to_string(); let i18n_oidc_provider_successfully_updated = i18n .t("oidc.oidc_provider_successfully_updated") .to_string(); let i18n_failed_to_update_provider = i18n.t("oidc.failed_to_update_provider").to_string(); let i18n_add_oidc_provider = i18n.t("oidc.add_oidc_provider").to_string(); let i18n_edit_oidc_provider = i18n.t("oidc.edit_oidc_provider").to_string(); let i18n_close_modal = i18n.t("common.close_modal").to_string(); let i18n_oidc_redirect_url = i18n.t("oidc.oidc_redirect_url").to_string(); let i18n_use_this_url_when_configuring = i18n.t("oidc.use_this_url_when_configuring").to_string(); let i18n_provider_name = i18n.t("oidc.provider_name").to_string(); let i18n_client_id = i18n.t("oidc.client_id").to_string(); let i18n_client_secret = i18n.t("oidc.client_secret").to_string(); let i18n_authorization_url = i18n.t("oidc.authorization_url").to_string(); let i18n_token_url = i18n.t("oidc.token_url").to_string(); let i18n_user_info_url = i18n.t("oidc.user_info_url").to_string(); let i18n_submit = i18n.t("common.submit").to_string(); let i18n_add = i18n.t("common.add").to_string(); let i18n_update = i18n.t("common.update").to_string(); let i18n_oidc_provider_management = i18n.t("oidc.oidc_provider_management").to_string(); let i18n_add_provider = i18n.t("oidc.add_provider").to_string(); let i18n_no_oidc_providers_configured = i18n.t("oidc.no_oidc_providers_configured").to_string(); let i18n_remove = i18n.t("common.remove").to_string(); // Form states for the add/edit provider modal let provider_name = use_state(|| String::new()); let client_id = use_state(|| String::new()); let client_secret = use_state(|| String::new()); let auth_url = use_state(|| String::new()); let token_url = use_state(|| String::new()); let user_info_url = use_state(|| String::new()); let button_text = use_state(|| String::new()); let button_color = use_state(|| String::from("#000000")); let button_text_color = use_state(|| String::from("#000000")); let icon_svg = use_state(|| String::new()); let name_claim = use_state(|| String::new()); let email_claim = use_state(|| String::new()); let username_claim = use_state(|| String::new()); let roles_claim = use_state(|| String::new()); let user_role = use_state(|| String::new()); let admin_role = use_state(|| String::new()); let selected_scopes = use_state(|| Vec::::new()); let editing_provider_id = use_state(|| None::); // Function to populate form with provider data for editing let populate_form_for_edit = { let provider_name = provider_name.clone(); let client_id = client_id.clone(); let client_secret = client_secret.clone(); let auth_url = auth_url.clone(); let token_url = token_url.clone(); let user_info_url = user_info_url.clone(); let button_text = button_text.clone(); let button_color = button_color.clone(); let button_text_color = button_text_color.clone(); let icon_svg = icon_svg.clone(); let name_claim = name_claim.clone(); let email_claim = email_claim.clone(); let username_claim = username_claim.clone(); let roles_claim = roles_claim.clone(); let user_role = user_role.clone(); let admin_role = admin_role.clone(); let selected_scopes = selected_scopes.clone(); let editing_provider_id = editing_provider_id.clone(); move |provider: &OIDCProvider| { provider_name.set(provider.provider_name.clone()); client_id.set(provider.client_id.clone()); client_secret.set(String::new()); // Don't populate secret for security auth_url.set(provider.authorization_url.clone()); token_url.set(provider.token_url.clone()); user_info_url.set(provider.user_info_url.clone()); button_text.set(provider.button_text.clone()); button_color.set(provider.button_color.clone()); button_text_color.set(provider.button_text_color.clone()); icon_svg.set(provider.icon_svg.as_ref().unwrap_or(&String::new()).clone()); name_claim.set( provider .name_claim .as_ref() .unwrap_or(&String::new()) .clone(), ); email_claim.set( provider .email_claim .as_ref() .unwrap_or(&String::new()) .clone(), ); username_claim.set( provider .username_claim .as_ref() .unwrap_or(&String::new()) .clone(), ); roles_claim.set( provider .roles_claim .as_ref() .unwrap_or(&String::new()) .clone(), ); user_role.set( provider .user_role .as_ref() .unwrap_or(&String::new()) .clone(), ); admin_role.set( provider .admin_role .as_ref() .unwrap_or(&String::new()) .clone(), ); editing_provider_id.set(Some(provider.provider_id)); // Parse scopes from the provider's scope string let scopes_vec: Vec = provider .scope .split_whitespace() .map(|s| s.to_string()) .collect(); selected_scopes.set(scopes_vec); } }; // Function to clear form for adding new provider let clear_form = { let provider_name = provider_name.clone(); let client_id = client_id.clone(); let client_secret = client_secret.clone(); let auth_url = auth_url.clone(); let token_url = token_url.clone(); let user_info_url = user_info_url.clone(); let button_text = button_text.clone(); let button_color = button_color.clone(); let button_text_color = button_text_color.clone(); let icon_svg = icon_svg.clone(); let name_claim = name_claim.clone(); let email_claim = email_claim.clone(); let username_claim = username_claim.clone(); let roles_claim = roles_claim.clone(); let user_role = user_role.clone(); let admin_role = admin_role.clone(); let selected_scopes = selected_scopes.clone(); let editing_provider_id = editing_provider_id.clone(); move || { provider_name.set(String::new()); client_id.set(String::new()); client_secret.set(String::new()); auth_url.set(String::new()); token_url.set(String::new()); user_info_url.set(String::new()); button_text.set(String::new()); button_color.set(String::from("#000000")); button_text_color.set(String::from("#000000")); icon_svg.set(String::new()); name_claim.set(String::new()); email_claim.set(String::new()); username_claim.set(String::new()); roles_claim.set(String::new()); user_role.set(String::new()); admin_role.set(String::new()); selected_scopes.set(Vec::new()); editing_provider_id.set(None); } }; // Add this callback after your other input handlers let scope_on_select = { let selected_scopes = selected_scopes.clone(); Callback::from(move |scopes: Vec| { selected_scopes.set(scopes); }) }; // Fetch providers on component mount and when update_trigger changes let effect_state = state.clone(); { let providers = providers.clone(); let update_trigger = update_trigger.clone(); let _dispatch = _dispatch.clone(); use_effect_with(*update_trigger, move |_| { let server_name = effect_state .auth_details .as_ref() .map(|ud| ud.server_name.clone()); let api_key = effect_state .auth_details .as_ref() .and_then(|ud| ud.api_key.clone()); if let (Some(server_name), Some(api_key)) = (server_name, api_key) { wasm_bindgen_futures::spawn_local(async move { match call_list_oidc_providers(server_name, api_key).await { Ok(fetched_providers) => { providers.set(fetched_providers); } Err(e) => { let formatted_error = format_error_message(&e.to_string()); _dispatch.reduce_mut(|state| { state.error_message = Some(format!( "{}{}", i18n_failed_to_fetch_oidc_providers.clone(), formatted_error )); }); } } }); } || () }); } let on_add_provider = { let page_state = page_state.clone(); let clear_form = clear_form.clone(); Callback::from(move |_| { clear_form(); page_state.set(PageState::AddProvider); }) }; let on_edit_provider = { let page_state = page_state.clone(); let populate_form_for_edit = populate_form_for_edit.clone(); let providers = providers.clone(); Callback::from(move |provider_id: i32| { if let Some(provider) = providers.iter().find(|p| p.provider_id == provider_id) { populate_form_for_edit(provider); page_state.set(PageState::EditProvider(provider_id)); } }) }; let on_close_modal = { let page_state = page_state.clone(); Callback::from(move |_: MouseEvent| { page_state.set(PageState::Hidden); }) }; let on_background_click = { let on_close_modal = on_close_modal.clone(); Callback::from(move |e: MouseEvent| { let target = e.target().unwrap(); let element = target.dyn_into::().unwrap(); if element.tag_name() == "DIV" { on_close_modal.emit(e); } }) }; let stop_propagation = Callback::from(|e: MouseEvent| { e.stop_propagation(); }); let remove_state = state.clone(); let on_remove_provider = { let update_trigger = update_trigger.clone(); let _dispatch = _dispatch.clone(); let i18n_provider_successfully_removed = i18n_provider_successfully_removed.clone(); let i18n_failed_to_remove_provider = i18n_failed_to_remove_provider.clone(); Callback::from(move |provider_id: i32| { let i18n_provider_successfully_removed = i18n_provider_successfully_removed.clone(); let i18n_failed_to_remove_provider = i18n_failed_to_remove_provider.clone(); let server_name = remove_state .auth_details .as_ref() .map(|ud| ud.server_name.clone()); let api_key = remove_state .auth_details .as_ref() .and_then(|ud| ud.api_key.clone()); let update_trigger = update_trigger.clone(); let _dispatch = _dispatch.clone(); if let (Some(server_name), Some(api_key)) = (server_name, api_key) { wasm_bindgen_futures::spawn_local(async move { match call_remove_oidc_provider(server_name, api_key, provider_id).await { Ok(_) => { update_trigger.set(!*update_trigger); _dispatch.reduce_mut(|state| { state.info_message = Some(i18n_provider_successfully_removed.clone()); }); } Err(e) => { let formatted_error = format_error_message(&e.to_string()); _dispatch.reduce_mut(|state| { state.error_message = Some(format!( "{}{}", i18n_failed_to_remove_provider, formatted_error )); }); } } }); } }) }; // Create provider modal // Input handlers let on_provider_name_change = { let provider_name = provider_name.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); provider_name.set(target.value()); }) }; let on_client_id_change = { let client_id = client_id.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); client_id.set(target.value()); }) }; let on_client_secret_change = { let client_secret = client_secret.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); client_secret.set(target.value()); }) }; let on_auth_url_change = { let auth_url = auth_url.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); auth_url.set(target.value()); }) }; let on_token_url_change = { let token_url = token_url.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); token_url.set(target.value()); }) }; let on_user_info_url_change = { let user_info_url = user_info_url.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); user_info_url.set(target.value()); }) }; let on_button_text_change = { let button_text = button_text.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); button_text.set(target.value()); }) }; let on_button_color_change = { let button_color = button_color.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); button_color.set(target.value()); }) }; let on_button_text_color_change = { let button_text_color = button_text_color.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); button_text_color.set(target.value()); }) }; let on_icon_svg_change = { let icon_svg = icon_svg.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); icon_svg.set(target.value()); }) }; let on_name_claim_change = { let name_claim = name_claim.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); name_claim.set(target.value()); }) }; let on_email_claim_change = { let email_claim = email_claim.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); email_claim.set(target.value()); }) }; let on_username_claim_change = { let username_claim = username_claim.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); username_claim.set(target.value()); }) }; let on_roles_claim_change = { let roles_claim = roles_claim.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); roles_claim.set(target.value()); }) }; let on_user_role_change = { let user_role = user_role.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); user_role.set(target.value()); }) }; let on_admin_role_change = { let admin_role = admin_role.clone(); Callback::from(move |e: InputEvent| { let target = e.target_unchecked_into::(); admin_role.set(target.value()); }) }; let submit_state = state.clone(); let on_submit = { let provider_name = provider_name.clone(); let client_id = client_id.clone(); let client_secret = client_secret.clone(); let auth_url = auth_url.clone(); let token_url = token_url.clone(); let user_info_url = user_info_url.clone(); let button_text = button_text.clone(); let button_color = button_color.clone(); let button_text_color = button_text_color.clone(); let icon_svg = icon_svg.clone(); let name_claim = name_claim.clone(); let email_claim = email_claim.clone(); let username_claim = username_claim.clone(); let roles_claim = roles_claim.clone(); let user_role = user_role.clone(); let admin_role = admin_role.clone(); let page_state = page_state.clone(); let update_trigger = update_trigger.clone(); let _dispatch = _dispatch.clone(); let selected_scopes = selected_scopes.clone(); let i18n_oidc_provider_successfully_added = i18n_oidc_provider_successfully_added.clone(); let i18n_failed_to_add_provider = i18n_failed_to_add_provider.clone(); let i18n_oidc_provider_successfully_updated = i18n_oidc_provider_successfully_updated.clone(); let i18n_failed_to_update_provider = i18n_failed_to_update_provider.clone(); Callback::from(move |e: SubmitEvent| { let i18n_oidc_provider_successfully_added = i18n_oidc_provider_successfully_added.clone(); let i18n_failed_to_add_provider = i18n_failed_to_add_provider.clone(); let i18n_oidc_provider_successfully_updated = i18n_oidc_provider_successfully_updated.clone(); let i18n_failed_to_update_provider = i18n_failed_to_update_provider.clone(); let call_trigger = update_trigger.clone(); let call_page_state = page_state.clone(); let call_dispatch = _dispatch.clone(); e.prevent_default(); // Calculate detected_provider inside the callback so it uses current values let detected_provider = detect_provider(&(*auth_url), &(*token_url), &(*user_info_url)); let provider = AddOIDCProviderRequest { provider_name: (*provider_name).clone(), client_id: (*client_id).clone(), client_secret: (*client_secret).clone(), authorization_url: (*auth_url).clone(), token_url: (*token_url).clone(), user_info_url: (*user_info_url).clone(), button_text: (*button_text).clone(), scope: Some(format_scopes_for_request( &*selected_scopes, &detected_provider, )), button_color: Some((*button_color).clone()), button_text_color: Some((*button_text_color).clone()), icon_svg: Some((*icon_svg).clone()), name_claim: Some((*name_claim).clone()), email_claim: Some((*email_claim).clone()), username_claim: Some((*username_claim).clone()), roles_claim: Some((*roles_claim).clone()), user_role: Some((*user_role).clone()), admin_role: Some((*admin_role).clone()), }; // Rest of your submission code... let server_name = submit_state .auth_details .as_ref() .map(|ud| ud.server_name.clone()); let api_key = submit_state .auth_details .as_ref() .and_then(|ud| ud.api_key.clone()); if let (Some(server_name), Some(api_key)) = (server_name, api_key) { let current_page_state = (*call_page_state).clone(); wasm_bindgen_futures::spawn_local(async move { match current_page_state { PageState::AddProvider => { match call_add_oidc_provider(server_name, api_key, provider).await { Ok(_) => { call_trigger.set(!*call_trigger); call_page_state.set(PageState::Hidden); call_dispatch.reduce_mut(|state| { state.info_message = Some(i18n_oidc_provider_successfully_added.clone()); }); } Err(e) => { let formatted_error = format_error_message(&e.to_string()); call_dispatch.reduce_mut(|state| { state.error_message = Some(format!( "{}{}", i18n_failed_to_add_provider, formatted_error )); }); } } } PageState::EditProvider(provider_id) => { match call_update_oidc_provider( server_name, api_key, provider_id, provider, ) .await { Ok(_) => { call_trigger.set(!*call_trigger); call_page_state.set(PageState::Hidden); call_dispatch.reduce_mut(|state| { state.info_message = Some(i18n_oidc_provider_successfully_updated.clone()); }); } Err(e) => { let formatted_error = format_error_message(&e.to_string()); call_dispatch.reduce_mut(|state| { state.error_message = Some(format!( "{}{}", i18n_failed_to_update_provider, formatted_error )); }); } } } PageState::Hidden => { // This shouldn't happen, but handle gracefully } } }); } }) }; let redirect_url = if let Some(auth_details) = &state.auth_details { format!("{}/api/auth/callback", auth_details.server_name) } else { "https://your-pinepods-instance/api/auth/callback".to_string() }; let onclick_copy = { let url = redirect_url.clone(); Callback::from(move |_| { if let Some(window) = web_sys::window() { let clipboard = window.navigator().clipboard(); let _ = clipboard.write_text(&url); } }) }; let add_provider_modal = html! {