Move certificate export and revoke actions into header

This commit is contained in:
Walter Oggioni
2026-10-27 07:39:00 +08:00
committed by woggioni
parent 73e6a11463
commit 917e0c1d7c
4 changed files with 155 additions and 67 deletions
+116 -62
View File
@@ -44,8 +44,13 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
let active_tab = create_signal(initial_tab());
let info = create_signal(None::<String>);
let openssl_config = create_signal(String::new());
let actions_open = create_signal(false);
let export_open = create_signal(false);
let revoke_open = create_signal(false);
let can_revoke = create_signal(false);
let load = move || {
can_revoke.set(false);
spawn_local(async move {
match api::certificate(id, cert_id_value).await {
Ok(value) => {
@@ -70,7 +75,17 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
purposes.set(value.purposes.clone());
chain.set(value.chain.clone());
openssl_config.set(value.openssl_config.clone());
let issuer_id = if value.summary.revoked {
None
} else {
value.summary.issuer_id.clone()
};
cert.set(Some(value));
if let Some(issuer_id) = issuer_id {
if let Ok(issuer) = api::certificate(id, &issuer_id).await {
can_revoke.set(issuer.summary.has_private_key);
}
}
if is_ca {
if let Ok(page) = api::templates(id).await {
templates.set(page.results);
@@ -104,12 +119,13 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
spawn_local(async move {
match api::export_certificate(id, cert_id_value, &payload).await {
Ok(payload) => {
if let Err(message) = dom::download_base64(
match dom::download_base64(
&payload.filename,
&payload.content_type,
&payload.content_base64,
) {
error.set(Some(message));
Ok(()) => export_open.set(false),
Err(message) => error.set(Some(message)),
}
}
Err(message) if message == api::LOGIN_REQUIRED => {}
@@ -119,11 +135,15 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
};
let revoke = move |_| {
if !can_revoke.get() {
return;
}
let payload = json!({"reason": rev_reason.get_clone()});
spawn_local(async move {
match api::revoke_certificate(id, cert_id_value, &payload).await {
Ok(_) => {
info.set(Some("Certificate revoked.".to_string()));
revoke_open.set(false);
load();
}
Err(message) if message == api::LOGIN_REQUIRED => {}
@@ -132,7 +152,13 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
});
};
let open_revoke = move |_| {
actions_open.set(false);
revoke_open.set(true);
};
let remove = move |_| {
actions_open.set(false);
if !dom::confirm("Delete this certificate?") {
return;
}
@@ -324,11 +350,26 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
format!("{} · serial {}", if is_ca { "CA certificate" } else { "end-entity certificate" }, header_serial),
view! {
a(class="btn btn-outline-light", href=format!("/vaults/{id}/certificates")) { "Back" }
a(class="btn btn-primary", href=format!("/vaults/{id}/certificates/{cert_id_value}/renew")) { "Renew" }
(if !summary.revoked && !is_ca {
view! { button(class="btn btn-warning", on:click=revoke) { "Revoke" } }
} else { view! {} })
button(class="btn btn-outline-danger", on:click=remove) { "Delete" }
button(class="btn btn-outline-light", r#type="button", on:click=move |_| {
actions_open.set(false);
export_open.set(true);
}) { "Export" }
div(class="dropdown") {
button(class="btn btn-primary dropdown-toggle", r#type="button", aria-haspopup="true",
aria-expanded=if actions_open.get() { "true" } else { "false" },
on:click=move |_| actions_open.set(!actions_open.get())) { "Actions" }
(if actions_open.get() {
view! {
div(class="dropdown-menu dropdown-menu-end show", role="menu") {
a(class="dropdown-item", href=format!("/vaults/{id}/certificates/{cert_id_value}/renew"), role="menuitem",
on:click=move |_| actions_open.set(false)) { "Renew" }
button(class="dropdown-item text-warning", r#type="button", role="menuitem",
disabled=!can_revoke.get(), on:click=open_revoke) { "Revoke" }
button(class="dropdown-item text-danger", r#type="button", role="menuitem", on:click=remove) { "Delete" }
}
}
} else { view! {} })
}
},
))
div(class="card mb-3") {
@@ -349,9 +390,7 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
(tab("overview", "Overview"))
(tab("extensions", "Extensions"))
(tab("validation", "Validation"))
(tab("export", "Export"))
(tab("metadata", "Metadata"))
(tab("revoke", "Revoke"))
(tab("transform", "Transform"))
(if is_ca { tab("ca", "CA") } else { view! {} })
}
@@ -411,42 +450,6 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
}
} else { view! {} })
(if active_tab.get_clone() == "export" {
view! {
div(id="panel-export", role="tabpanel", aria-labelledby="tab-export", class="row g-3") {
div(class="col-12 col-lg-6") {
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="pem-chain") { "PEM chain" }
option(value="pem-key") { "PEM + key" }
option(value="pem-pkcs8") { "PEM + encrypted PKCS#8" }
option(value="pkcs7") { "PKCS#7" }
option(value="pkcs7-chain") { "PKCS#7 chain" }
option(value="der") { "DER" }
option(value="pkcs12") { "PKCS#12" }
option(value="pkcs12-chain") { "PKCS#12 chain" }
option(value="openvpn") { "OpenVPN" }
option(value="openssl-config") { "OpenSSL config" }
option(value="index") { "Certificate index" }
option(value="vcalendar") { "vCalendar" }
option(value="jwk") { "JWK" }
option(value="jwk-chain") { "JWK chain" }
option(value="tls-auth") { "OpenVPN tls-auth key" }
}
div(class="mb-2") {
label(class="form-label") { "Password (encrypted formats)" }
input(class="form-control", r#type="password", bind:value=export_password)
}
button(class="btn btn-primary", on:click=do_export) { "Download" }
}
}
}
}
}
} else { view! {} })
(if active_tab.get_clone() == "metadata" {
view! {
div(id="panel-metadata", role="tabpanel", aria-labelledby="tab-metadata", class="row g-3") {
@@ -469,21 +472,6 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
}
}
} else { view! {} })
(if active_tab.get_clone() == "revoke" {
view! {
div(id="panel-revoke", role="tabpanel", aria-labelledby="tab-revoke", class="row g-3") {
div(class="col-12 col-lg-6") {
div(class="card") {
div(class="card-header") { "Revoke" }
div(class="card-body") {
select(class="form-select mb-2", bind:value=rev_reason) { (reason_options()) }
button(class="btn btn-warning", disabled=summary.revoked, on:click=revoke) { "Revoke certificate" }
}
}
}
}
}
} else { view! {} })
(if active_tab.get_clone() == "transform" {
view! {
div(id="panel-transform", role="tabpanel", aria-labelledby="tab-transform", class="row g-3") {
@@ -596,6 +584,71 @@ pub fn CertificateDetailPage(vault_id: String, cert_id: String) -> View {
} else {
view! {}
})
(if export_open.get() {
view! {
div(class="modal fade show d-block", tabindex="-1", role="dialog", aria-modal="true", aria-labelledby="export-dialog-title") {
div(class="modal-dialog modal-dialog-centered") {
div(class="modal-content") {
div(class="modal-header") {
h2(class="modal-title h5", id="export-dialog-title") { "Export certificate" }
button(class="btn-close btn-close-white", r#type="button", aria-label="Close", on:click=move |_| export_open.set(false))
}
div(class="modal-body") {
label(class="form-label", r#for="export-format") { "Format" }
select(id="export-format", class="form-select mb-3", bind:value=export_format) {
option(value="pem") { "PEM" }
option(value="pem-chain") { "PEM chain" }
option(value="pem-key") { "PEM + key" }
option(value="pem-pkcs8") { "PEM + encrypted PKCS#8" }
option(value="pkcs7") { "PKCS#7" }
option(value="pkcs7-chain") { "PKCS#7 chain" }
option(value="der") { "DER" }
option(value="pkcs12") { "PKCS#12" }
option(value="pkcs12-chain") { "PKCS#12 chain" }
option(value="openvpn") { "OpenVPN" }
option(value="openssl-config") { "OpenSSL config" }
option(value="index") { "Certificate index" }
option(value="vcalendar") { "vCalendar" }
option(value="jwk") { "JWK" }
option(value="jwk-chain") { "JWK chain" }
option(value="tls-auth") { "OpenVPN tls-auth key" }
}
label(class="form-label", r#for="export-password") { "Password (encrypted formats)" }
input(id="export-password", class="form-control", r#type="password", bind:value=export_password)
}
div(class="modal-footer") {
button(class="btn btn-outline-light", r#type="button", on:click=move |_| export_open.set(false)) { "Cancel" }
button(class="btn btn-primary", r#type="button", on:click=do_export) { "Download" }
}
}
}
}
div(class="modal-backdrop fade show")
}
} else { view! {} })
(if revoke_open.get() {
view! {
div(class="modal fade show d-block", tabindex="-1", role="dialog", aria-modal="true", aria-labelledby="revoke-dialog-title") {
div(class="modal-dialog modal-dialog-centered") {
div(class="modal-content") {
div(class="modal-header") {
h2(class="modal-title h5", id="revoke-dialog-title") { "Revoke certificate" }
button(class="btn-close btn-close-white", r#type="button", aria-label="Close", on:click=move |_| revoke_open.set(false))
}
div(class="modal-body") {
label(class="form-label", r#for="revoke-reason") { "Reason" }
select(id="revoke-reason", class="form-select", bind:value=rev_reason) { (reason_options()) }
}
div(class="modal-footer") {
button(class="btn btn-outline-light", r#type="button", on:click=move |_| revoke_open.set(false)) { "Cancel" }
button(class="btn btn-warning", r#type="button", disabled=!can_revoke.get(), on:click=revoke) { "Revoke certificate" }
}
}
}
}
div(class="modal-backdrop fade show")
}
} else { view! {} })
}
};
@@ -620,8 +673,9 @@ fn initial_tab() -> String {
.unwrap_or_default();
let hash = hash.trim_start_matches('#');
match hash {
"overview" | "extensions" | "validation" | "export" | "metadata" | "revoke"
| "transform" | "ca" => hash.to_string(),
"overview" | "extensions" | "validation" | "metadata" | "transform" | "ca" => {
hash.to_string()
}
_ => "overview".to_string(),
}
}