feat: add backup dialog

This commit is contained in:
reya 2024-03-06 14:40:00 +07:00
parent 95294a80cb
commit 71a2290b8f
5 changed files with 121 additions and 51 deletions

View File

@ -1,13 +1,36 @@
import { CancelIcon } from "@lume/icons"; import { ArrowRightIcon, CancelIcon } from "@lume/icons";
import * as Dialog from "@radix-ui/react-dialog"; import * as Dialog from "@radix-ui/react-dialog";
import { Link, useParams } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
export function BackupDialog() { export function BackupDialog() {
const [key, setKey] = useState(""); // @ts-ignore, magic!!!
const { account } = useParams({ strict: false });
const [key, setKey] = useState(null);
const [passphase, setPassphase] = useState(""); const [passphase, setPassphase] = useState("");
const [loading, setLoading] = useState(false);
const encryptKey = async () => { const encryptKey = async () => {
console.log("****"); try {
setLoading(true);
const encrypted: string = await invoke("get_encrypted_key", {
npub: account,
password: passphase,
});
if (encrypted) {
setKey(encrypted);
}
setLoading(false);
} catch (e) {
setLoading(false);
toast.error(String(e));
}
}; };
return ( return (
@ -41,20 +64,7 @@ export function BackupDialog() {
</div> </div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label htmlFor="nsec"> <label htmlFor="nsec">Set a passphase to secure your key</label>
Copy this key and keep it in safe place
</label>
<input
name="nsec"
type="text"
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
<span className="font-semibold">(Recommend)</span> Set a
passphase to secure your key
</label>
<div className="relative"> <div className="relative">
<input <input
name="passphase" name="passphase"
@ -63,19 +73,45 @@ export function BackupDialog() {
onChange={(e) => setPassphase(e.target.value)} onChange={(e) => setPassphase(e.target.value)}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900" className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/> />
{passphase.length ? (
<div className="absolute right-2 top-0 h-11 py-2">
<button
type="button"
onClick={encryptKey}
className="inline-flex h-full items-center justify-center rounded-md bg-blue-500 px-3 text-sm font-medium text-white hover:bg-blue-600"
>
Update
</button>
</div>
) : null}
</div> </div>
</div> </div>
{key ? (
<div className="flex flex-col gap-2">
<label htmlFor="nsec">
Copy this key and keep it in safe place
</label>
<input
name="nsec"
type="text"
value={key}
readOnly
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
/>
</div>
) : null}
</div>
<div className="flex flex-col gap-3">
{!key ? (
<button
type="button"
onClick={encryptKey}
disabled={loading}
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
<div className="size-5" />
<div>Submit</div>
<ArrowRightIcon className="size-5" />
</button>
) : (
<Link
to="/$account/home"
params={{ account }}
search={{ guest: false }}
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
I've safely store my account key
</Link>
)}
</div> </div>
</div> </div>
</Dialog.Content> </Dialog.Content>

View File

@ -22,15 +22,17 @@ export class Ark {
public async get_all_accounts() { public async get_all_accounts() {
try { try {
const accounts: Account[] = []; const accounts: Account[] = [];
const cmd: string[] = await invoke("get_all_nsecs"); const cmd: string[] = await invoke("get_accounts");
for (const item of cmd) { if (cmd) {
accounts.push({ npub: item.replace(".npub", "") }); for (const item of cmd) {
accounts.push({ npub: item.replace(".npub", "") });
}
this.accounts = accounts;
return accounts;
} }
} catch {
this.accounts = accounts;
return accounts;
} catch (e) {
return []; return [];
} }
} }

View File

@ -49,10 +49,10 @@ pub async fn show_in_folder(path: String) {
} }
#[tauri::command] #[tauri::command]
pub fn get_all_nsecs(app_handle: tauri::AppHandle) -> Result<Vec<String>, ()> { pub fn get_accounts(app_handle: tauri::AppHandle) -> Result<Vec<String>, ()> {
let dir = app_handle.path().app_config_dir().unwrap(); let dir = app_handle.path().home_dir().unwrap();
if let Ok(paths) = std::fs::read_dir(dir) { if let Ok(paths) = std::fs::read_dir(dir.join("Lume/")) {
let files = paths let files = paths
.filter_map(|res| res.ok()) .filter_map(|res| res.ok())
.map(|dir_entry| dir_entry.path()) .map(|dir_entry| dir_entry.path())

View File

@ -7,6 +7,8 @@ pub mod commands;
pub mod nostr; pub mod nostr;
pub mod tray; pub mod tray;
use std::fs;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use tauri::Manager; use tauri::Manager;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
@ -20,11 +22,15 @@ fn main() {
.setup(|app| { .setup(|app| {
let _tray = tray::create_tray(app.handle()).unwrap(); let _tray = tray::create_tray(app.handle()).unwrap();
let handle = app.handle().clone(); let handle = app.handle().clone();
let config_dir = handle.path().app_config_dir().unwrap(); let resource_dir = handle.path().resource_dir().unwrap();
let home_dir = handle.path().home_dir().unwrap();
// create data folder if not exist
fs::create_dir_all(home_dir.join("Lume/")).unwrap();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
// Create nostr database connection // Create nostr database connection
let nostr_db = SQLiteDatabase::open(config_dir.join("nostr.db")) let nostr_db = SQLiteDatabase::open(resource_dir.join("lume.db"))
.await .await
.expect("Open database failed."); .expect("Open database failed.");
@ -79,6 +85,7 @@ fn main() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
nostr::keys::create_keys, nostr::keys::create_keys,
nostr::keys::save_key, nostr::keys::save_key,
nostr::keys::get_encrypted_key,
nostr::keys::verify_signer, nostr::keys::verify_signer,
nostr::keys::load_selected_account, nostr::keys::load_selected_account,
nostr::keys::event_to_bech32, nostr::keys::event_to_bech32,
@ -108,15 +115,21 @@ fn main() {
nostr::event::upvote, nostr::event::upvote,
nostr::event::downvote, nostr::event::downvote,
commands::folder::show_in_folder, commands::folder::show_in_folder,
commands::folder::get_all_nsecs, commands::folder::get_accounts,
commands::opg::fetch_opg, commands::opg::fetch_opg,
]) ])
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while running tauri application") .expect("error while running tauri application")
.run(|_app_handle, event| match event { .run(|app, event| {
tauri::RunEvent::ExitRequested { api, .. } => { if let tauri::RunEvent::Opened { urls } = event {
api.prevent_exit(); if let Some(w) = app.get_webview_window("main") {
let urls = urls
.iter()
.map(|u| u.as_str())
.collect::<Vec<_>>()
.join(",");
let _ = w.eval(&format!("window.onFileOpen(`{urls}`)"));
}
} }
_ => {}
}); });
} }

View File

@ -53,13 +53,14 @@ pub async fn save_key(
let npub = nostr_keys.public_key().to_bech32().unwrap(); let npub = nostr_keys.public_key().to_bech32().unwrap();
let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap(); let nsec = nostr_keys.secret_key().unwrap().to_bech32().unwrap();
if let Ok(config_dir) = app_handle.path().app_config_dir() { let home_dir = app_handle.path().home_dir().unwrap();
let file_path = npub.clone() + ".npub"; let app_dir = home_dir.join("Lume/");
let _ = File::create(config_dir.join(file_path)).unwrap();
let keyring = Entry::new("Lume Secret Storage", &npub).unwrap(); let file_path = npub.clone() + ".npub";
let _ = keyring.set_password(&nsec); let _ = File::create(app_dir.join(file_path)).unwrap();
}
let keyring = Entry::new("Lume Secret Storage", &npub).unwrap();
let _ = keyring.set_password(&nsec);
let signer = NostrSigner::Keys(nostr_keys); let signer = NostrSigner::Keys(nostr_keys);
let client = &state.client; let client = &state.client;
@ -84,6 +85,24 @@ pub async fn verify_signer(state: State<'_, Nostr>) -> Result<bool, ()> {
} }
} }
#[tauri::command]
pub fn get_encrypted_key(npub: &str, password: &str) -> Result<String, String> {
let keyring = Entry::new("Lume Secret Storage", npub).unwrap();
if let Ok(nsec) = keyring.get_password() {
let secret_key = SecretKey::from_bech32(nsec).expect("Get secret key failed");
let new_key = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium);
if let Ok(key) = new_key {
Ok(key.to_bech32().unwrap())
} else {
Err("Encrypt key failed".into())
}
} else {
Err("Key not found".into())
}
}
#[tauri::command] #[tauri::command]
pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> { pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client; let client = &state.client;