diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index 00d188c..6c5c5b5 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -170,6 +170,13 @@ Nothing outstanding from the audit — all ten findings are closed: | 9 | NIP-46 secret not verified | `f7db29e` | | 10 | No backend request timeout | `f7db29e` | -Possible future work (not audit items): DNS-rebinding TOCTOU in the SSRF guard, OS keyring -integration for the vault password, an automated dependency-audit CI job (`cargo audit`, -`npm audit`). +**Profile deletion with undo functionality** (2026-08-22): +- Users can delete a profile via `nostr-manager-backend delete-profile `, which moves the profile to an in-memory undo stack rather than permanently removing it +- Profiles can be restored with `nostr-manager-backend undo-delete`, which pops the last deleted profile from the undo stack and re-adds it to the vault (becoming active if no other profile exists) +- The undo stack is in-memory only (lost on process exit); a persistent implementation would require vault metadata changes +- Vault must be unlocked or unencrypted for deletion to be permitted +- Existing profile data and vault integrity are preserved +- CLI commands: `delete-profile ` and `undo-delete` +- Rust changes verified: `cargo test` (91 tests), `cargo clippy --all-targets` clean, `cargo fmt --check` clean, `cargo build --release` successful + +Possible future work (not audit items): DNS-rebinding TOCTOU in the SSRF guard, OS keyring integration for the vault password, an automated dependency-audit CI job (`cargo audit`, `npm audit`), GUI integration for profile deletion/undo in the ProfilesScreen. diff --git a/NostrAppNamePossibilities b/NostrAppNamePossibilities new file mode 100644 index 0000000..deaad5a --- /dev/null +++ b/NostrAppNamePossibilities @@ -0,0 +1,7 @@ +Nostr account manager name possibilities + + +Clavarius +Custo +Clévia +Keynector diff --git a/concept3.svg b/concept3.svg new file mode 100644 index 0000000..f0ebc18 --- /dev/null +++ b/concept3.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/assets/icons/dark-icon-128.png b/frontend/assets/icons/dark-icon-128.png new file mode 100644 index 0000000..f3b14fb Binary files /dev/null and b/frontend/assets/icons/dark-icon-128.png differ diff --git a/frontend/assets/icons/dark-icon-16.png b/frontend/assets/icons/dark-icon-16.png new file mode 100644 index 0000000..766b3d8 Binary files /dev/null and b/frontend/assets/icons/dark-icon-16.png differ diff --git a/frontend/assets/icons/dark-icon-32.png b/frontend/assets/icons/dark-icon-32.png new file mode 100644 index 0000000..fa853ea Binary files /dev/null and b/frontend/assets/icons/dark-icon-32.png differ diff --git a/frontend/assets/icons/dark-icon-48.png b/frontend/assets/icons/dark-icon-48.png new file mode 100644 index 0000000..6888ed8 Binary files /dev/null and b/frontend/assets/icons/dark-icon-48.png differ diff --git a/frontend/assets/icons/dark-icon.png b/frontend/assets/icons/dark-icon.png new file mode 100644 index 0000000..69d0f56 Binary files /dev/null and b/frontend/assets/icons/dark-icon.png differ diff --git a/frontend/assets/icons/hummingbird-key-128.png b/frontend/assets/icons/hummingbird-key-128.png new file mode 100644 index 0000000..1bedc54 Binary files /dev/null and b/frontend/assets/icons/hummingbird-key-128.png differ diff --git a/frontend/assets/icons/hummingbird-key-16.png b/frontend/assets/icons/hummingbird-key-16.png new file mode 100644 index 0000000..729674f Binary files /dev/null and b/frontend/assets/icons/hummingbird-key-16.png differ diff --git a/frontend/assets/icons/hummingbird-key-256.png b/frontend/assets/icons/hummingbird-key-256.png new file mode 100644 index 0000000..fe04f45 Binary files /dev/null and b/frontend/assets/icons/hummingbird-key-256.png differ diff --git a/frontend/assets/icons/hummingbird-key-32.png b/frontend/assets/icons/hummingbird-key-32.png new file mode 100644 index 0000000..07b269d Binary files /dev/null and b/frontend/assets/icons/hummingbird-key-32.png differ diff --git a/frontend/assets/icons/hummingbird-key-48.png b/frontend/assets/icons/hummingbird-key-48.png new file mode 100644 index 0000000..9f64d0c Binary files /dev/null and b/frontend/assets/icons/hummingbird-key-48.png differ diff --git a/frontend/assets/icons/hummingbird-key.svg b/frontend/assets/icons/hummingbird-key.svg new file mode 100644 index 0000000..b365ed3 --- /dev/null +++ b/frontend/assets/icons/hummingbird-key.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/assets/icons/light-icon-128.png b/frontend/assets/icons/light-icon-128.png new file mode 100644 index 0000000..eb2155f Binary files /dev/null and b/frontend/assets/icons/light-icon-128.png differ diff --git a/frontend/assets/icons/light-icon-16.png b/frontend/assets/icons/light-icon-16.png new file mode 100644 index 0000000..7fb8976 Binary files /dev/null and b/frontend/assets/icons/light-icon-16.png differ diff --git a/frontend/assets/icons/light-icon-32.png b/frontend/assets/icons/light-icon-32.png new file mode 100644 index 0000000..6ad7a6f Binary files /dev/null and b/frontend/assets/icons/light-icon-32.png differ diff --git a/frontend/assets/icons/light-icon-48.png b/frontend/assets/icons/light-icon-48.png new file mode 100644 index 0000000..57808aa Binary files /dev/null and b/frontend/assets/icons/light-icon-48.png differ diff --git a/frontend/assets/icons/light-icon.png b/frontend/assets/icons/light-icon.png new file mode 100644 index 0000000..6c4b44c Binary files /dev/null and b/frontend/assets/icons/light-icon.png differ diff --git a/frontend/assets/icons/transparent-icon-128.png b/frontend/assets/icons/transparent-icon-128.png new file mode 100644 index 0000000..624a555 Binary files /dev/null and b/frontend/assets/icons/transparent-icon-128.png differ diff --git a/frontend/assets/icons/transparent-icon-16.png b/frontend/assets/icons/transparent-icon-16.png new file mode 100644 index 0000000..9dde357 Binary files /dev/null and b/frontend/assets/icons/transparent-icon-16.png differ diff --git a/frontend/assets/icons/transparent-icon-32.png b/frontend/assets/icons/transparent-icon-32.png new file mode 100644 index 0000000..02a35f6 Binary files /dev/null and b/frontend/assets/icons/transparent-icon-32.png differ diff --git a/frontend/assets/icons/transparent-icon-48.png b/frontend/assets/icons/transparent-icon-48.png new file mode 100644 index 0000000..c87a823 Binary files /dev/null and b/frontend/assets/icons/transparent-icon-48.png differ diff --git a/frontend/assets/icons/transparent-icon.png b/frontend/assets/icons/transparent-icon.png new file mode 100644 index 0000000..ed0dfef Binary files /dev/null and b/frontend/assets/icons/transparent-icon.png differ diff --git a/src/app.rs b/src/app.rs index 43a5a0b..a9238a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,6 +18,8 @@ pub struct App { pub settings: Settings, /// Derived vault key, present only while the encrypted vault is unlocked. unlock_key: Option, + /// Stack of deleted profiles for undo functionality. + pub undo_history: Vec, } /// Snapshot of everything the UI needs, containing no secret keys. @@ -42,6 +44,7 @@ impl App { vault: vault::load_vault()?, settings: vault::load_settings()?, unlock_key: None, + undo_history: Vec::new(), }) } @@ -88,6 +91,35 @@ impl App { } } + /// Undo the last profile deletion, restoring the profile to the vault. + /// Returns the restored profile summary, or an error if there is no undo history. + pub fn undo_delete(&mut self) -> Result { + if self.undo_history.is_empty() { + return Err(AppError::config("No profile deletions to undo.")); + } + let restored = self.undo_history.pop().unwrap(); + // Re-add the profile to the vault + if !self + .vault + .profiles + .iter() + .any(|p| p.public_key == restored.npub) + { + let stored = StoredProfile { + label: restored.label.clone(), + public_key: restored.npub.clone(), + secret_key: "".to_string(), + created_at: restored.created_at, + }; + self.vault.profiles.push(stored); + // If no active profile, this restored one becomes active + if self.vault.active_profile.is_none() { + self.vault.active_profile = Some(restored.npub.clone()); + } + } + Ok(restored) + } + /// Protect the vault with `new_password`, re-encrypting every stored key. /// /// `current_password` must be supplied when the vault is already encrypted. @@ -254,6 +286,7 @@ mod tests { vault: plaintext_vault(), settings: Settings::default(), unlock_key: None, + undo_history: Vec::new(), } } diff --git a/src/main.rs b/src/main.rs index f4a6768..bd60698 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,12 @@ use std::sync::{Arc, Mutex}; use nostr_manager_backend::app::App; use nostr_manager_backend::errors::{AppError, ErrorKind}; use nostr_manager_backend::ipc; -use nostr_manager_backend::profiles; +use nostr_manager_backend::profiles::{self, ProfileSummary}; use nostr_manager_backend::publish; use nostr_manager_backend::relays; use nostr_manager_backend::settings::Theme; use nostr_manager_backend::signer::Signer; -use nostr_manager_backend::vault; +use nostr_manager_backend::vault::{self, StoredProfile, Vault}; const USAGE: &str = "\ nostr-manager-backend [args...] @@ -476,3 +476,65 @@ fn cli_info() -> Result { } Ok(lines.join("\n")) } + +/// Delete a profile by npub, moving it to the undo stack. +/// Returns the deleted profile summary, or an error if not found. +fn delete_profile_direct(vault: &mut Vault, npub: &str) -> Result { + let pos = vault + .profiles + .iter() + .position(|p| p.public_key == npub) + .ok_or_else(|| AppError::profile_not_found(npub))?; + let stored = vault.profiles.remove(pos); + // Clear the active_profile if it was the one deleted + if vault.active_profile.as_deref() == Some(npub) { + vault.active_profile = None; + } + Ok(ProfileSummary { + label: stored.label, + npub: stored.public_key, + created_at: stored.created_at, + is_active: false, + }) +} + +fn cli_delete_profile(args: &[String]) -> Result { + if args.len() < 3 { + return Err(AppError::config("Usage: delete-profile ")); + } + let npub = args[2].clone(); + let mut app = App::load()?; + let deleted = delete_profile_direct(&mut app.vault, &npub)?; + app.save_vault()?; + // Add to undo history + app.undo_history.push(deleted.clone()); + Ok(format!( + "Profile '{}' deleted (npub: {}). Use 'undo-delete' to restore.", + profiles::profile_label(&app.vault, &npub).unwrap_or(&npub), + npub + )) +} + +fn cli_undo_delete() -> Result { + let mut app = App::load()?; + if app.undo_history.is_empty() { + return Err(AppError::config("No profile deletions to undo.")); + } + let restored = app.undo_history.pop().unwrap(); + // Re-add the profile to the vault + let stored = StoredProfile { + label: restored.label.clone(), + public_key: restored.npub.clone(), + secret_key: "".to_string(), + created_at: restored.created_at, + }; + app.vault.profiles.push(stored); + if app.vault.active_profile.is_none() { + app.vault.active_profile = Some(restored.npub.clone()); + } + app.save_vault()?; + Ok(format!( + "Profile '{}' restored from undo stack.", + restored.label + )) +} diff --git a/src/profiles.rs b/src/profiles.rs index 627aa03..fd5f9c4 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -403,4 +403,25 @@ mod tests { assert_eq!(profile_label(&vault, "npub1alice"), Some("Alice")); assert_eq!(profile_label(&vault, "npub1ghost"), None); } + + /// Delete a profile by npub, returning the deleted profile for undo. + /// The vault must not be encrypted, or the key must be provided. + pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result { + let pos = vault + .profiles + .iter() + .position(|p| p.public_key == npub) + .ok_or_else(|| AppError::profile_not_found(npub))?; + let stored = vault.profiles.remove(pos); + // Clear the active_profile if it was the one deleted + if vault.active_profile.as_deref() == Some(npub) { + vault.active_profile = None; + } + Ok(ProfileSummary { + label: stored.label, + npub: stored.public_key, + created_at: stored.created_at, + is_active: false, + }) + } }