//! Per-vault options, mirroring XCA's database settings. use serde_json::json; use sycamore::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api; use crate::components::crypto::{key_parameter_input, key_type_select, signature_hash_input}; use crate::components::nav::{app_nav, spinner, whoami}; use crate::components::ui::{page_header, toast_error}; use crate::model::{Me, OptionsPage}; pub fn SettingsPage(vault_id: String) -> View { let id: &'static str = Box::leak(vault_id.clone().into_boxed_str()); let me = create_signal(None::); let options = create_signal(None::); let error = create_signal(None::); let info = create_signal(None::); let loading = create_signal(true); let saving = create_signal(false); let version = create_signal(0i64); let default_hash = create_signal("sha256".to_string()); let serial_len = create_signal("64".to_string()); let explicit_dn = create_signal("C,ST,L,O,OU,CN,emailAddress".to_string()); let mandatory_dn = create_signal(String::new()); let disable_netscape = create_signal(false); let hide_unusable = create_signal(false); let default_key_type = create_signal("rsa".to_string()); let default_key_size = create_signal("2048".to_string()); let default_key_curve = create_signal("secp256r1".to_string()); let default_validity = create_signal("365".to_string()); let default_validity_unit = create_signal("days".to_string()); spawn_local(async move { if let Ok(user) = api::me().await { me.set(Some(user)); } if let Ok(page) = api::options(id).await { options.set(Some(page)); } match api::vault_settings(&id).await { Ok(value) => { version.set(value.get("version").and_then(|v| v.as_i64()).unwrap_or(0)); if let Some(settings) = value.get("settings") { if let Some(v) = settings.get("default_hash").and_then(|v| v.as_str()) { default_hash.set(v.to_string()); } if let Some(v) = settings.get("serial_len_bits").and_then(|v| v.as_i64()) { serial_len.set(v.to_string()); } if let Some(items) = settings.get("explicit_dn").and_then(|v| v.as_array()) { explicit_dn.set(items.iter().filter_map(|v| v.as_str()).collect::>().join(",")); } if let Some(items) = settings.get("mandatory_dn").and_then(|v| v.as_array()) { mandatory_dn.set(items.iter().filter_map(|v| v.as_str()).collect::>().join(",")); } disable_netscape.set(settings.get("disable_netscape").and_then(|v| v.as_bool()).unwrap_or(false)); hide_unusable.set(settings.get("hide_unusable").and_then(|v| v.as_bool()).unwrap_or(false)); if let Some(key) = settings.get("default_key") { if let Some(v) = key.get("type").and_then(|v| v.as_str()) { default_key_type.set(v.to_string()); } if let Some(v) = key.get("size").and_then(|v| v.as_i64()) { default_key_size.set(v.to_string()); } if let Some(v) = key.get("curve").and_then(|v| v.as_str()) { default_key_curve.set(v.to_string()); } } if let Some(validity) = settings.get("default_validity") { if let Some(v) = validity.get("amount").and_then(|v| v.as_i64()) { default_validity.set(v.to_string()); } if let Some(v) = validity.get("unit").and_then(|v| v.as_str()) { default_validity_unit.set(v.to_string()); } } } } Err(message) => error.set(Some(message)), } loading.set(false); }); let save = { let id = id.clone(); move |_| { let id = id.clone(); let settings = json!({ "default_hash": default_hash.get_clone(), "serial_len_bits": serial_len.get_clone().parse::().unwrap_or(64), "explicit_dn": explicit_dn.get_clone().split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect::>(), "mandatory_dn": mandatory_dn.get_clone().split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect::>(), "disable_netscape": disable_netscape.get(), "hide_unusable": hide_unusable.get(), "default_key": { "type": default_key_type.get_clone(), "size": default_key_size.get_clone().parse::().unwrap_or(2048), "curve": if default_key_type.get_clone() == "ec" { json!(default_key_curve.get_clone()) } else { serde_json::Value::Null }, }, "default_validity": { "amount": default_validity.get_clone().parse::().unwrap_or(365), "unit": default_validity_unit.get_clone(), }, }); saving.set(true); spawn_local(async move { match api::update_vault_settings(&id, &settings, version.get()).await { Ok(value) => { version.set(value.get("version").and_then(|v| v.as_i64()).unwrap_or(version.get())); info.set(Some("Settings saved.".to_string())); } Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } saving.set(false); }); } }; let content = move || { if loading.get() { return spinner(); } view! { (page_header( "Vault settings", "These options are stored with the vault and mirror XCA's database preferences.", view! { a(class="btn btn-outline-light", href=format!("/vaults/{id}")) { "Back" } }, )) (if let Some(message) = info.get_clone() { view! { div(class="alert alert-success") { (message) } } } else { view! {} }) div(class="card") { div(class="card-body") { div(class="form-section") { div(class="form-section-title") { "Cryptography" } div(class="row g-3") { div(class="col-md-4") { (signature_hash_input( "Default signature hash", options, || None, default_hash, )) } div(class="col-md-4") { label(class="form-label") { "Serial number length (bits)" } input(class="form-control", bind:value=serial_len) } div(class="col-md-4") { label(class="form-label") { "Default validity" } div(class="input-group") { input(class="form-control", bind:value=default_validity) select(class="form-select", bind:value=default_validity_unit) { option(value="days") { "Days" } option(value="months") { "Months" } option(value="years") { "Years" } } } } } } div(class="form-section") { div(class="form-section-title") { "Distinguished name" } div(class="row g-3") { div(class="col-md-6") { label(class="form-label") { "Explicit subject entries" } input(class="form-control", bind:value=explicit_dn) div(class="form-text") { "Comma-separated short names, e.g. C,ST,L,O,OU,CN,emailAddress." } } div(class="col-md-6") { label(class="form-label") { "Mandatory subject entries" } input(class="form-control", bind:value=mandatory_dn) } } } div(class="form-section") { div(class="form-section-title") { "Editor defaults" } div(class="row g-3") { div(class="col-md-4") { label(class="form-label") { "Default key type" } (key_type_select(options, default_key_type)) } div(class="col-md-4") { (key_parameter_input( options, default_key_type, default_key_size, default_key_curve, )) } div(class="col-md-4 d-flex align-items-end") { button(class="btn btn-primary", disabled=saving.get(), on:click=save) { (if saving.get() { "Saving…" } else { "Save settings" }) } } } div(class="form-check mt-3") { input(class="form-check-input", r#type="checkbox", bind:checked=disable_netscape) label(class="form-check-label") { "Disable legacy Netscape extensions" } } div(class="form-check") { input(class="form-check-input", r#type="checkbox", bind:checked=hide_unusable) label(class="form-check-label") { "Hide expired and revoked certificates by default" } } } } } } }; let nav = move || match me.get_clone() { Some(user) => whoami(&user), None => view! {}, }; view! { (app_nav("/", Some(id), nav)) main(class="container page-shell py-4") { (toast_error(error)) (content) } } }