Files
kca/web/src/pages/settings.rs
T
woggioni 1a7b4a10e8
CI / Build and push docker image (push) Successful in 37s
CI / Build and push OCSP image (push) Successful in 38s
Add full XCA curve and digest parity
2026-10-26 12:06:17 +08:00

232 lines
11 KiB
Rust

//! 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::<Me>);
let options = create_signal(None::<OptionsPage>);
let error = create_signal(None::<String>);
let info = create_signal(None::<String>);
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::<Vec<_>>().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::<Vec<_>>().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::<i64>().unwrap_or(64),
"explicit_dn": explicit_dn.get_clone().split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect::<Vec<_>>(),
"mandatory_dn": mandatory_dn.get_clone().split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect::<Vec<_>>(),
"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::<i64>().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::<i64>().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)
}
}
}