feat: bitcoin-ui CSS fix, HTTPS proxy support, deploy script improvements
Bitcoin UI: - Replace cdn.tailwindcss.com with locally bundled tailwind.css (CSP blocks external scripts) - Make all asset paths relative for nginx proxy compatibility - Add bitcoin-ui build/deploy to deploy-to-target.sh (was missing entirely) - Use --network host (bitcoin-ui proxies Bitcoin RPC at 127.0.0.1:8332) HTTPS mixed content fix: - Add HTTPS_PROXY_PATHS in AppSession.vue — when parent page is HTTPS, iframe loads through nginx proxy instead of direct HTTP port - Prevents browser blocking HTTP iframes inside HTTPS pages - All Tailscale servers use HTTPS, this was breaking all app iframes Deploy & first-boot improvements: - first-boot-containers.sh auto-detects disk size for pruning vs txindex - first-boot-containers.sh checks fallback source path for UI containers - Added mempool-electrs to APP_PORTS mapping - ElectrumX container creation in first-boot - Podman doctor/fix/uptime skills added Also includes: session persistence, identity management, LND transactions, ElectrumX status UI, nostr-provider improvements, Web5 enhancements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4e54b8bd4d
commit
367b483a72
@@ -48,6 +48,28 @@ pub struct IdentityRecord {
|
||||
pub nostr_pubkey: Option<String>,
|
||||
/// Nostr public key in bech32 npub format (NIP-19)
|
||||
pub nostr_npub: Option<String>,
|
||||
/// Nostr profile metadata (NIP-01 kind 0)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<IdentityProfile>,
|
||||
}
|
||||
|
||||
/// Nostr profile metadata fields (NIP-01 kind 0 + NIP-24 extra fields).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct IdentityProfile {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub about: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub picture: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub banner: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub website: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nip05: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub lud16: Option<String>,
|
||||
}
|
||||
|
||||
/// On-disk format for identity storage (includes secret key bytes).
|
||||
@@ -64,6 +86,9 @@ struct IdentityFile {
|
||||
nostr_secret_hex: Option<String>,
|
||||
#[serde(default)]
|
||||
nostr_pubkey_hex: Option<String>,
|
||||
/// Nostr profile metadata
|
||||
#[serde(default)]
|
||||
profile: Option<IdentityProfile>,
|
||||
}
|
||||
|
||||
pub struct IdentityManager {
|
||||
@@ -123,6 +148,7 @@ impl IdentityManager {
|
||||
created_at: created_at.clone(),
|
||||
nostr_secret_hex: None,
|
||||
nostr_pubkey_hex: None,
|
||||
profile: None,
|
||||
};
|
||||
|
||||
let file_path = self.identities_dir.join(format!("{}.json", id));
|
||||
@@ -345,6 +371,77 @@ impl IdentityManager {
|
||||
.context("NIP-44 decryption failed")
|
||||
}
|
||||
|
||||
/// Update the profile metadata for an identity.
|
||||
pub async fn update_profile(&self, id: &str, profile: IdentityProfile) -> Result<()> {
|
||||
let file_path = self.identities_dir.join(format!("{}.json", id));
|
||||
if !file_path.exists() {
|
||||
return Err(anyhow::anyhow!("Identity not found: {}", id));
|
||||
}
|
||||
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
|
||||
let mut file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
|
||||
file.profile = Some(profile);
|
||||
let json = serde_json::to_string_pretty(&file).context("Failed to serialize identity")?;
|
||||
fs::write(&file_path, json.as_bytes()).await.context("Failed to write identity file")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish kind 0 (metadata) event to a Nostr relay.
|
||||
pub async fn publish_profile(&self, id: &str, relay_url: &str) -> Result<String> {
|
||||
let record = self.get(id).await?;
|
||||
let keys = self.load_nostr_keys(id).await?;
|
||||
let profile = record.profile.unwrap_or_default();
|
||||
|
||||
// Build kind 0 content JSON (NIP-01 + NIP-24)
|
||||
let mut content = serde_json::Map::new();
|
||||
content.insert("name".to_string(), serde_json::json!(record.name));
|
||||
if let Some(v) = &profile.display_name { content.insert("display_name".to_string(), serde_json::json!(v)); }
|
||||
if let Some(v) = &profile.about { content.insert("about".to_string(), serde_json::json!(v)); }
|
||||
if let Some(v) = &profile.picture { content.insert("picture".to_string(), serde_json::json!(v)); }
|
||||
if let Some(v) = &profile.banner { content.insert("banner".to_string(), serde_json::json!(v)); }
|
||||
if let Some(v) = &profile.website { content.insert("website".to_string(), serde_json::json!(v)); }
|
||||
if let Some(v) = &profile.nip05 { content.insert("nip05".to_string(), serde_json::json!(v)); }
|
||||
if let Some(v) = &profile.lud16 { content.insert("lud16".to_string(), serde_json::json!(v)); }
|
||||
|
||||
let content_str = serde_json::to_string(&content).context("Failed to serialize profile content")?;
|
||||
|
||||
let client = nostr_sdk::Client::new(keys);
|
||||
client.add_relay(relay_url).await.context("Failed to add relay")?;
|
||||
client.connect().await;
|
||||
|
||||
let builder = nostr_sdk::prelude::EventBuilder::new(
|
||||
nostr_sdk::prelude::Kind::Metadata,
|
||||
&content_str,
|
||||
);
|
||||
let output = client.send_event_builder(builder).await.context("Failed to publish profile")?;
|
||||
client.disconnect().await;
|
||||
|
||||
Ok(output.id().to_hex())
|
||||
}
|
||||
|
||||
/// Export all keys for an identity (SENSITIVE — only call after password verification).
|
||||
pub async fn export_keys(&self, id: &str) -> Result<serde_json::Value> {
|
||||
let file_path = self.identities_dir.join(format!("{}.json", id));
|
||||
if !file_path.exists() {
|
||||
return Err(anyhow::anyhow!("Identity not found: {}", id));
|
||||
}
|
||||
let data = fs::read(&file_path).await.context("Failed to read identity file")?;
|
||||
let file: IdentityFile = serde_json::from_slice(&data).context("Failed to parse identity file")?;
|
||||
|
||||
let ed25519_secret_hex = hex::encode(&file.secret_key);
|
||||
|
||||
let nostr_nsec = file.nostr_secret_hex.as_ref().and_then(|h| {
|
||||
nostr_sdk::SecretKey::from_hex(h)
|
||||
.ok()
|
||||
.and_then(|sk| sk.to_bech32().ok())
|
||||
});
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"ed25519_secret_hex": ed25519_secret_hex,
|
||||
"nostr_secret_hex": file.nostr_secret_hex,
|
||||
"nostr_nsec": nostr_nsec,
|
||||
}))
|
||||
}
|
||||
|
||||
// --- internal helpers ---
|
||||
|
||||
}
|
||||
@@ -395,6 +492,7 @@ impl IdentityManager {
|
||||
created_at: file.created_at,
|
||||
nostr_pubkey: file.nostr_pubkey_hex,
|
||||
nostr_npub,
|
||||
profile: file.profile,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user