From 8eaf47f6d2105f15e11a07dde26d5af0b217d758 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 6 Mar 2024 09:42:44 +0700 Subject: [PATCH] feat: add login dialog --- apps/desktop2/package.json | 1 + apps/desktop2/src/components/accounts.tsx | 25 ++--- apps/desktop2/src/components/backup.tsx | 85 +++++++++++++++ apps/desktop2/src/components/login.tsx | 120 ++++++++++++++++++++++ apps/desktop2/src/routes/$account.tsx | 7 ++ apps/desktop2/src/routes/backup.lazy.tsx | 20 ---- apps/desktop2/src/routes/index.tsx | 2 +- apps/desktop2/src/routes/login.tsx | 14 --- packages/ark/src/ark.ts | 42 ++++---- packages/icons/src/arrowRight.tsx | 23 ++--- packages/icons/src/cancel.tsx | 22 ++-- packages/icons/src/settings.tsx | 12 ++- pnpm-lock.yaml | 12 ++- src-tauri/src/main.rs | 1 - src-tauri/src/nostr/keys.rs | 88 ++++++++-------- src-tauri/src/tray.rs | 14 +-- src-tauri/tauri.conf.json | 15 ++- 17 files changed, 336 insertions(+), 167 deletions(-) create mode 100644 apps/desktop2/src/components/backup.tsx create mode 100644 apps/desktop2/src/components/login.tsx delete mode 100644 apps/desktop2/src/routes/backup.lazy.tsx delete mode 100644 apps/desktop2/src/routes/login.tsx diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 256e1f49..763359f7 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -15,6 +15,7 @@ "@lume/utils": "workspace:^", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-popover": "^1.0.7", "@tanstack/query-sync-storage-persister": "^5.24.1", diff --git a/apps/desktop2/src/components/accounts.tsx b/apps/desktop2/src/components/accounts.tsx index dbf134fd..de125ef6 100644 --- a/apps/desktop2/src/components/accounts.tsx +++ b/apps/desktop2/src/components/accounts.tsx @@ -4,9 +4,9 @@ import { User } from "@lume/ui"; import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import * as Popover from "@radix-ui/react-popover"; -import { Link } from "@tanstack/react-router"; -import { useTranslation } from "react-i18next"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { BackupDialog } from "./backup"; +import { LoginDialog } from "./login"; export function Accounts() { const ark = useArk(); @@ -63,7 +63,6 @@ function Active({ pubkey }: { pubkey: string }) { const [open, setOpen] = useState(true); // @ts-ignore, magic !!! const { guest } = useSearch({ strict: false }); - const { t } = useTranslation(); if (guest) { return ( @@ -84,25 +83,17 @@ function Active({ pubkey }: { pubkey: string }) { side="bottom" >
-

You're using guest account

+

+ You're using random account +

You can continue by claim and backup this account, or you can - import your own account key. + import your own account.

- - Claim & Backup - - - {t("welcome.login")} - + +
diff --git a/apps/desktop2/src/components/backup.tsx b/apps/desktop2/src/components/backup.tsx new file mode 100644 index 00000000..c124e665 --- /dev/null +++ b/apps/desktop2/src/components/backup.tsx @@ -0,0 +1,85 @@ +import { CancelIcon } from "@lume/icons"; +import * as Dialog from "@radix-ui/react-dialog"; +import { useState } from "react"; + +export function BackupDialog() { + const [key, setKey] = useState(""); + const [passphase, setPassphase] = useState(""); + + const encryptKey = async () => { + console.log("****"); + }; + + return ( + + + + + + + + + + + Esc + + +
+
+

+ This is your account key +

+

+ It's use for login to Lume or other Nostr clients. You will lost + access to your account if you lose this key. +

+
+
+
+ + +
+
+ +
+ 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" + /> + {passphase.length ? ( +
+ +
+ ) : null} +
+
+
+
+
+
+
+ ); +} diff --git a/apps/desktop2/src/components/login.tsx b/apps/desktop2/src/components/login.tsx new file mode 100644 index 00000000..0d8cf6f2 --- /dev/null +++ b/apps/desktop2/src/components/login.tsx @@ -0,0 +1,120 @@ +import { useArk } from "@lume/ark"; +import { ArrowRightIcon, CancelIcon } from "@lume/icons"; +import * as Dialog from "@radix-ui/react-dialog"; +import { useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { toast } from "sonner"; + +export function LoginDialog() { + const ark = useArk(); + const navigate = useNavigate(); + + const [nsec, setNsec] = useState(""); + const [passphase, setPassphase] = useState(""); + const [loading, setLoading] = useState(false); + + const login = async () => { + try { + setLoading(true); + + const save = await ark.save_account(nsec, passphase); + + if (save) { + navigate({ to: "/", search: { guest: false } }); + } + } catch (e) { + setLoading(false); + toast.error(String(e)); + } + }; + + return ( + + + + + + + + + + + Esc + + +
+
+

Add new account with

+
+ + + +
+
+
+
+ + setNsec(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" + /> +
+
+ + 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" + /> +
+
+
+ +
+
+
+
+
+ ); +} diff --git a/apps/desktop2/src/routes/$account.tsx b/apps/desktop2/src/routes/$account.tsx index 546d0a3d..bf3defaf 100644 --- a/apps/desktop2/src/routes/$account.tsx +++ b/apps/desktop2/src/routes/$account.tsx @@ -5,6 +5,7 @@ import { HomeFilledIcon, HomeIcon, HorizontalDotsIcon, + SettingsIcon, SpaceFilledIcon, SpaceIcon, } from "@lume/icons"; @@ -43,6 +44,12 @@ function App() { New post + diff --git a/apps/desktop2/src/routes/backup.lazy.tsx b/apps/desktop2/src/routes/backup.lazy.tsx deleted file mode 100644 index 31cdd902..00000000 --- a/apps/desktop2/src/routes/backup.lazy.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createLazyFileRoute } from "@tanstack/react-router"; -import { useTranslation } from "react-i18next"; - -export const Route = createLazyFileRoute("/backup")({ - component: Screen, -}); - -function Screen() { - const { t } = useTranslation(); - - return ( -
-
-
-

{t("backup.title")}

-
-
-
- ); -} diff --git a/apps/desktop2/src/routes/index.tsx b/apps/desktop2/src/routes/index.tsx index 760b708a..98d6fa86 100644 --- a/apps/desktop2/src/routes/index.tsx +++ b/apps/desktop2/src/routes/index.tsx @@ -6,7 +6,7 @@ import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; export const Route = createFileRoute("/")({ - beforeLoad: async ({ location, context }) => { + beforeLoad: async ({ context }) => { const ark = context.ark; const accounts = await ark.get_all_accounts(); diff --git a/apps/desktop2/src/routes/login.tsx b/apps/desktop2/src/routes/login.tsx deleted file mode 100644 index 4bc1d153..00000000 --- a/apps/desktop2/src/routes/login.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Outlet, createFileRoute } from "@tanstack/react-router"; - -export const Route = createFileRoute("/login")({ - component: Screen, -}); - -function Screen() { - return ( -
-

Login

- -
- ); -} diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index af64ad67..400d0c9a 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -31,7 +31,6 @@ export class Ark { this.accounts = accounts; return accounts; } catch (e) { - console.error(e); return []; } } @@ -45,19 +44,18 @@ export class Ark { return cmd; } catch (e) { - console.error(e); - return false; + throw new Error(String(e)); } } public async create_guest_account() { try { const keys = await this.create_keys(); - await this.save_account(keys); + await this.save_account(keys.nsec, ""); return keys.npub; } catch (e) { - console.error(e); + throw new Error(String(e)); } } @@ -70,17 +68,20 @@ export class Ark { } } - public async save_account(keys: Keys) { + public async save_account(nsec: string, password: string = "") { try { - const cmd: boolean = await invoke("save_key", { nsec: keys.nsec }); + const cmd: boolean = await invoke("save_key", { + nsec, + password, + }); if (cmd) { - await invoke("update_signer", { nsec: keys.nsec }); + await invoke("update_signer", { nsec }); } return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } @@ -92,7 +93,7 @@ export class Ark { }); return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } @@ -106,7 +107,7 @@ export class Ark { const event: Event = JSON.parse(cmd); return event; } catch (e) { - return null; + throw new Error(String(e)); } } @@ -210,8 +211,7 @@ export class Ark { return cmd; } catch (e) { - console.error(String(e)); - return false; + throw new Error(String(e)); } } @@ -220,7 +220,7 @@ export class Ark { const cmd: string = await invoke("reply_to", { content, tags }); return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } @@ -229,7 +229,7 @@ export class Ark { const cmd: string = await invoke("repost", { id, pubkey: author }); return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } @@ -238,7 +238,7 @@ export class Ark { const cmd: string = await invoke("upvote", { id, pubkey: author }); return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } @@ -247,7 +247,7 @@ export class Ark { const cmd: string = await invoke("downvote", { id, pubkey: author }); return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } @@ -366,8 +366,7 @@ export class Ark { const cmd: string = await invoke("follow", { id, alias }); return cmd; } catch (e) { - console.error(e); - return false; + throw new Error(String(e)); } } @@ -376,8 +375,7 @@ export class Ark { const cmd: string = await invoke("unfollow", { id }); return cmd; } catch (e) { - console.error(e); - return false; + throw new Error(String(e)); } } @@ -389,7 +387,7 @@ export class Ark { }); return cmd; } catch (e) { - console.error(String(e)); + throw new Error(String(e)); } } diff --git a/packages/icons/src/arrowRight.tsx b/packages/icons/src/arrowRight.tsx index 33ebdc07..99ffaa37 100644 --- a/packages/icons/src/arrowRight.tsx +++ b/packages/icons/src/arrowRight.tsx @@ -1,18 +1,13 @@ -export function ArrowRightIcon(props: JSX.IntrinsicElements['svg']) { +export function ArrowRightIcon(props: JSX.IntrinsicElements["svg"]) { return ( - - + + ); } diff --git a/packages/icons/src/cancel.tsx b/packages/icons/src/cancel.tsx index b46e69ba..df63aad5 100644 --- a/packages/icons/src/cancel.tsx +++ b/packages/icons/src/cancel.tsx @@ -1,18 +1,12 @@ -export function CancelIcon(props: JSX.IntrinsicElements['svg']) { +export function CancelIcon(props: JSX.IntrinsicElements["svg"]) { return ( - - + + ); } diff --git a/packages/icons/src/settings.tsx b/packages/icons/src/settings.tsx index 02f98a47..ac69645d 100644 --- a/packages/icons/src/settings.tsx +++ b/packages/icons/src/settings.tsx @@ -7,14 +7,16 @@ export function SettingsIcon( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 738bc3fa..57a975da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: '@radix-ui/react-collapsible': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) @@ -226,7 +229,7 @@ importers: version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': specifier: ^1.0.5 - version: 1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) @@ -874,7 +877,7 @@ importers: version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dialog': specifier: ^1.0.5 - version: 1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) + version: 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 version: 2.0.6(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) @@ -1912,7 +1915,7 @@ packages: '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.61)(react@18.2.0) - '@radix-ui/react-dialog': 1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0) '@types/react': 18.2.61 @@ -2072,7 +2075,7 @@ packages: react: 18.2.0 dev: false - /@radix-ui/react-dialog@1.0.5(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.2.19)(@types/react@18.2.61)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} peerDependencies: '@types/react': '*' @@ -2099,6 +2102,7 @@ packages: '@radix-ui/react-slot': 1.0.2(@types/react@18.2.61)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.61)(react@18.2.0) '@types/react': 18.2.61 + '@types/react-dom': 18.2.19 aria-hidden: 1.2.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6dae87f6..fed4fc13 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -89,7 +89,6 @@ fn main() { .invoke_handler(tauri::generate_handler![ nostr::keys::create_keys, nostr::keys::save_key, - nostr::keys::get_public_key, nostr::keys::update_signer, nostr::keys::verify_signer, nostr::keys::load_selected_account, diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index 1a528986..fd358f5e 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -30,54 +30,58 @@ pub fn create_keys() -> Result { #[tauri::command] pub async fn save_key( nsec: &str, + password: &str, app_handle: tauri::AppHandle, state: State<'_, Nostr>, -) -> Result { - if let Ok(nostr_secret_key) = SecretKey::from_bech32(nsec) { - let nostr_keys = Keys::new(nostr_secret_key); - let nostr_npub = nostr_keys.public_key().to_bech32().unwrap(); - let signer = NostrSigner::Keys(nostr_keys); +) -> Result { + let secret_key: Result; - // Update client's signer - let client = &state.client; - client.set_signer(Some(signer)).await; - - let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap(); - let secret_key = keyring_entry.get_password().unwrap(); - let app_key = age::x25519::Identity::from_str(&secret_key).unwrap(); - let app_pubkey = app_key.to_public(); - - let config_dir = app_handle.path().app_config_dir().unwrap(); - let encryptor = - age::Encryptor::with_recipients(vec![Box::new(app_pubkey)]).expect("we provided a recipient"); - - let file_ext = ".nsec".to_owned(); - let file_path = nostr_npub + &file_ext; - let mut file = File::create(config_dir.join(file_path)).unwrap(); - let mut writer = encryptor - .wrap_output(&mut file) - .expect("Init writer failed"); - writer - .write_all(nsec.as_bytes()) - .expect("Write nsec failed"); - writer.finish().expect("Save nsec failed"); - - Ok(true) + if nsec.starts_with("ncrypto") { + let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap(); + secret_key = match encrypted_key.to_secret_key(password) { + Ok(val) => Ok(val), + Err(_) => Err("Wrong passphase".into()), + }; } else { - Ok(false) + secret_key = match SecretKey::from_bech32(nsec) { + Ok(val) => Ok(val), + Err(_) => Err("nsec is not valid".into()), + } } -} -#[tauri::command] -pub fn get_public_key(nsec: &str) -> Result { - let secret_key = SecretKey::from_bech32(nsec).unwrap(); - let keys = Keys::new(secret_key); - Ok( - keys - .public_key() - .to_bech32() - .expect("get public key failed"), - ) + match secret_key { + Ok(val) => { + let nostr_keys = Keys::new(val); + let nostr_npub = nostr_keys.public_key().to_bech32().unwrap(); + let signer = NostrSigner::Keys(nostr_keys); + + // Update client's signer + let client = &state.client; + client.set_signer(Some(signer)).await; + + let keyring_entry = Entry::new("Lume Secret Storage", "AppKey").unwrap(); + let master_key = keyring_entry.get_password().unwrap(); + let app_key = age::x25519::Identity::from_str(&master_key).unwrap(); + let app_pubkey = app_key.to_public(); + + let config_dir = app_handle.path().app_config_dir().unwrap(); + let encryptor = age::Encryptor::with_recipients(vec![Box::new(app_pubkey)]) + .expect("we provided a recipient"); + + let file_path = nostr_npub + ".nsec"; + let mut file = File::create(config_dir.join(file_path)).unwrap(); + let mut writer = encryptor + .wrap_output(&mut file) + .expect("Init writer failed"); + writer + .write_all(nsec.as_bytes()) + .expect("Write nsec failed"); + writer.finish().expect("Save nsec failed"); + + Ok(true) + } + Err(msg) => Err(msg.into()), + } } #[tauri::command] diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index d1716725..9dbca7c7 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -1,22 +1,12 @@ use tauri::{tray::ClickType, Manager, Runtime}; pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { + let tray = app.tray().unwrap(); let menu = tauri::menu::MenuBuilder::new(app) .item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap()) .build() .unwrap(); - - let tray = tauri::tray::TrayIconBuilder::with_id("main_tray") - .tooltip("Lume") - .icon(tauri::Icon::Rgba { - rgba: include_bytes!("../icons/icon.png").to_vec(), - width: 500, - height: 500, - }) - .icon_as_template(true) - .menu(&menu) - .build(app) - .unwrap(); + let _ = tray.set_menu(Some(menu)); tray.on_menu_event(move |app, event| match event.id.0.as_str() { "quit" => { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9b0e8f19..bbfb108a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -12,6 +12,11 @@ "app": { "macOSPrivateApi": true, "withGlobalTauri": true, + "trayIcon": { + "id": "main_tray", + "iconPath": "./icons/tray.png", + "iconAsTemplate": true + }, "security": { "assetProtocol": { "enable": true, @@ -92,7 +97,15 @@ "type": "downloadBootstrapper" }, "wix": null - } + }, + "fileAssociations": [ + { + "name": "bech32", + "description": "Nostr Bech32", + "ext": ["nsec", "nprofile", "nevent", "naddr", "nrelay"], + "role": "Viewer" + } + ] }, "plugins": { "updater": {