//! Certificate signing request detail. use serde_json::json; use sycamore::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api; use crate::components::nav::{app_nav, spinner, whoami}; use crate::components::ui::{empty_state, page_header, toast_error}; use crate::dom; use crate::model::{fmt_fp, Me, RequestDetail}; pub fn RequestDetailPage(vault_id: String, request_id: String) -> View { let id: &'static str = Box::leak(vault_id.clone().into_boxed_str()); let req_id: &'static str = Box::leak(request_id.clone().into_boxed_str()); let me = create_signal(None::); let request = create_signal(None::); let error = create_signal(None::); let loading = create_signal(true); let name = create_signal(String::new()); let comment = create_signal(String::new()); let request_name = create_signal(String::new()); let request_subject = create_signal(String::new()); let signature_label = create_signal(String::new()); let export_format = create_signal("pem".to_string()); spawn_local(async move { if let Ok(user) = api::me().await { me.set(Some(user)); } match api::request(id, req_id).await { Ok(value) => { name.set(value.summary.name.clone()); comment.set(value.summary.comment.clone()); request_name.set(value.summary.name.clone()); request_subject.set(value.summary.subject.clone()); signature_label.set(if value.signature_valid { "PKCS#10 signature valid".to_string() } else { "PKCS#10 signature INVALID".to_string() }); request.set(Some(value)); } Err(message) => error.set(Some(message)), } loading.set(false); }); let save = move |_| { let value = request.get_clone(); let Some(value) = value else { return }; let payload = json!({"name": name.get_clone(), "comment": comment.get_clone(), "version": value.summary.version}); spawn_local(async move { match api::update_request(id, req_id, &payload).await { Ok(updated) => { request_name.set(updated.summary.name.clone()); request.set(Some(updated)); } Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } }); }; let export = move |_| { let fmt = export_format.get_clone(); spawn_local(async move { match api::export_request(id, req_id, &json!({"format": fmt})).await { Ok(payload) => { if let Err(message) = dom::download_base64(&payload.filename, &payload.content_type, &payload.content_base64) { error.set(Some(message)); } } Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } }); }; let remove = move |_| { if !dom::confirm("Delete this certificate signing request?") { return; } spawn_local(async move { match api::delete_request(id, req_id).await { Ok(()) => sycamore_router::navigate(&format!("/vaults/{id}/requests")), Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } }); }; let extract = move |_| { spawn_local(async move { match api::transform_request(id, req_id, "open-key").await { Ok(value) => { if let Some(key_id) = value.get("id").and_then(|v| v.as_str()) { sycamore_router::navigate(&format!("/vaults/{id}/keys/{key_id}")); } } Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } }); }; let to_template = move |_| { spawn_local(async move { match api::transform_request(id, req_id, "template").await { Ok(value) => { if let Some(template_id) = value.get("id").and_then(|v| v.as_str()) { sycamore_router::navigate(&format!("/vaults/{id}/templates/{template_id}")); } } Err(message) if message == api::LOGIN_REQUIRED => {} Err(message) => error.set(Some(message)), } }); }; let content = move || { if loading.get() { return spinner(); } let Some(value) = request.get_clone() else { return empty_state("Request not found."); }; let mut html = String::new(); html.push_str(""); html.push_str(&format!("", escape(&value.subject_rfc4514))); html.push_str(&format!("", escape(&value.signature_algorithm))); html.push_str(&format!("", fmt_fp(&value.summary.fingerprint_sha256))); html.push_str(&format!("", value.summary.certificate_count)); html.push_str("
Subject{}
Signature{}
Fingerprint{}
Certificates issued{}
"); let mut subject_html = String::new(); for entry in &value.subject_entries { subject_html.push_str(&format!("{}{}", escape(&entry.name), escape(&entry.value))); } let mut ext_html = String::new(); for ext in &value.extensions { ext_html.push_str(&format!( "{}{}{}", escape(&ext.name), if ext.critical { " critical" } else { "" }, escape(&ext.value), )); } let mut attr_html = String::new(); for attr in &value.attributes { attr_html.push_str(&format!("{}{}", escape(&attr.name), escape(&attr.values.join(", ")))); } view! { (page_header(request_name.get_clone(), request_subject.get_clone(), view! { a(class="btn btn-primary", href=format!("/vaults/{id}/certificates/new?request={req_id}")) { "Sign request" } a(class="btn btn-outline-light", href=format!("/vaults/{id}/requests")) { "Back" } button(class="btn btn-outline-danger", on:click=remove) { "Delete" } })) div(class="alert alert-info") { (signature_label.get_clone()) } div(class="row g-3") { div(class="col-12 col-lg-7") { div(class="card mb-3") { div(class="card-header") { "Request" } div(class="card-body", dangerously_set_inner_html=html) {} } div(class="card mb-3") { div(class="card-header") { "Subject" } table(class="table table-sm mb-0") { tbody(dangerously_set_inner_html=subject_html) {} } } div(class="card mb-3") { div(class="card-header") { "Requested extensions" } div(class="table-responsive") { table(class="table table-sm ext-table mb-0") { thead() { tr() { th() { "Extension" } th() { "Value" } } } tbody(dangerously_set_inner_html=ext_html) {} } } } div(class="card") { div(class="card-header") { "Attributes" } table(class="table table-sm mb-0") { tbody(dangerously_set_inner_html=attr_html) {} } } } div(class="col-12 col-lg-5") { div(class="card mb-3") { div(class="card-header") { "Edit" } div(class="card-body") { div(class="mb-2") { label(class="form-label") { "Name" } input(class="form-control", bind:value=name) } div(class="mb-2") { label(class="form-label") { "Comment" } textarea(class="form-control", rows="3", bind:value=comment) {} } button(class="btn btn-primary", on:click=save) { "Save" } } } div(class="card mb-3") { div(class="card-header") { "Export" } div(class="card-body") { select(class="form-select mb-2", bind:value=export_format) { option(value="pem") { "PEM" } option(value="der") { "DER" } option(value="openssl-config") { "OpenSSL config" } } button(class="btn btn-primary", on:click=export) { "Download" } } } div(class="card") { div(class="card-header") { "Transform" } div(class="card-body d-grid gap-2") { button(class="btn btn-outline-light", on:click=extract) { "Extract public key" } button(class="btn btn-outline-light", on:click=to_template) { "Create template" } } } } } } }; 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) } } } fn escape(value: &str) -> String { value.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """) }