diff --git a/core/archipelago/src/api/rpc/assistant_chat.rs b/core/archipelago/src/api/rpc/assistant_chat.rs index 60c6896e..331cfa23 100644 --- a/core/archipelago/src/api/rpc/assistant_chat.rs +++ b/core/archipelago/src/api/rpc/assistant_chat.rs @@ -344,6 +344,7 @@ impl RpcHandler { "content.indeehub-projects" => self.handle_content_indeehub_projects().await, "system.settings.get" => self.handle_system_settings_get(params).await, "system.settings.set" => self.handle_system_settings_set(params).await, + "system.node-ca.generate" => self.handle_system_node_ca_generate().await, "system.kiosk-display.get" => self.handle_system_kiosk_display_get().await, "system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await, "bitcoin.relay-update-settings" => { diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index 9cfb5460..f68fc812 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -503,6 +503,7 @@ impl RpcHandler { "ai.permissions.set" => self.handle_ai_permissions_set(params).await, "system.settings.get" => self.handle_system_settings_get(params).await, "system.settings.set" => self.handle_system_settings_set(params).await, + "system.node-ca.generate" => self.handle_system_node_ca_generate().await, "system.kiosk-display.get" => self.handle_system_kiosk_display_get().await, "system.kiosk-display.set" => self.handle_system_kiosk_display_set(params).await, diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 9622c72f..9f89721b 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -1248,6 +1248,30 @@ impl RpcHandler { } } + /// system.node-ca.generate — run the node's (idempotent) CA setup so the + /// dashboard can offer certificate generation as a button. WebUI rule: + /// users must never be pointed at a terminal; the script reuses an + /// existing CA and only reissues the leaf, so re-running is safe. + pub(in crate::api::rpc) async fn handle_system_node_ca_generate( + &self, + ) -> Result { + let script = "/opt/archipelago/scripts/setup-node-ca.sh"; + if tokio::fs::metadata(script).await.is_err() { + anyhow::bail!( + "The certificate setup script is not on this node yet — it arrives with the next update." + ); + } + let status = host_sudo(&["/usr/bin/bash", script]).await?; + if !status.success() { + anyhow::bail!( + "Certificate generation failed (exit {:?}) — see the node log for detail", + status.code() + ); + } + info!("Node CA generated/reissued via dashboard"); + Ok(serde_json::json!({ "generated": true })) + } + /// system.kiosk-display.get — Current kiosk display preset + whether this /// node has a kiosk at all (no kiosk unit -> the Settings section hides). pub(in crate::api::rpc) async fn handle_system_kiosk_display_get( diff --git a/neode-ui/src/views/settings/NodeCertificateSection.vue b/neode-ui/src/views/settings/NodeCertificateSection.vue index 87a18359..2cf8a490 100644 --- a/neode-ui/src/views/settings/NodeCertificateSection.vue +++ b/neode-ui/src/views/settings/NodeCertificateSection.vue @@ -26,7 +26,30 @@ async function computeFingerprint(pem: string): Promise { .join(':') } -onMounted(async () => { +const generating = ref(false) +const generateError = ref('') + +// WebUI rule: never point a user at a terminal — the backend runs the +// (idempotent) CA setup script for us. +async function generateCa() { + generating.value = true + generateError.value = '' + try { + const { rpcClient } = await import('@/api/rpc-client') + await rpcClient.call({ method: 'system.node-ca.generate', timeout: 60000 }) + loading.value = true + await probe() + if (!caAvailable.value) { + generateError.value = 'Generation reported success but the certificate is not being served yet — try reloading in a few seconds.' + } + } catch (e: unknown) { + generateError.value = e instanceof Error ? e.message : 'Certificate generation failed' + } finally { + generating.value = false + } +} + +async function probe() { try { const res = await fetch('/ca.crt', { cache: 'no-store' }) if (!res.ok) throw new Error(`HTTP ${res.status}`) @@ -48,7 +71,9 @@ onMounted(async () => { } finally { loading.value = false } -}) +} + +onMounted(probe)