From e9fe71784a575ee7e2d84c78c00e4494996fb081 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Tue, 18 Aug 2026 18:28:38 +0200 Subject: [PATCH] New profile --- src-tauri/src/commands/accounts.rs | 48 ++++++++++ src-tauri/src/commands/mod.rs | 2 + .../{commands.rs => commands/server_list.rs} | 0 src-tauri/src/lib.rs | 6 +- src/App.tsx | 8 +- src/app/app.ts | 3 + src/components/view/NewProfileView.tsx | 89 +++++++++++++++++++ src/lib/accounts.ts | 19 ++++ 8 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/commands/accounts.rs create mode 100644 src-tauri/src/commands/mod.rs rename src-tauri/src/{commands.rs => commands/server_list.rs} (100%) create mode 100644 src/components/view/NewProfileView.tsx create mode 100644 src/lib/accounts.ts diff --git a/src-tauri/src/commands/accounts.rs b/src-tauri/src/commands/accounts.rs new file mode 100644 index 0000000..0be3371 --- /dev/null +++ b/src-tauri/src/commands/accounts.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use tauri::{AppHandle, Manager}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Account { + pub display_name: String, + pub private_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsFile { + pub active_account: Option, + pub accounts: Vec, +} + +fn accounts_file_path(app: &AppHandle) -> Result { + let dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to resolve app data dir: {e}"))?; + fs::create_dir_all(&dir).map_err(|e| format!("Failed to create app data dir: {e}"))?; + Ok(dir.join("accounts.json")) +} + +#[tauri::command] +pub fn save_accounts(app: AppHandle, data: AccountsFile) -> Result<(), String> { + let path = accounts_file_path(&app)?; + let json = serde_json::to_string_pretty(&data) + .map_err(|e| format!("Failed to serialize accounts: {e}"))?; + fs::write(&path, json).map_err(|e| format!("Failed to write accounts: {e}"))?; + Ok(()) +} + +#[tauri::command] +pub fn get_accounts(app: AppHandle) -> Result { + let path = accounts_file_path(&app)?; + if !path.exists() { + return Ok(AccountsFile { + active_account: None, + accounts: Vec::new(), + }); + } + let json = fs::read_to_string(&path).map_err(|e| format!("Failed to read accounts: {e}"))?; + serde_json::from_str(&json).map_err(|e| format!("Failed to parse accounts: {e}")) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..c714d1c --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod accounts; +pub mod server_list; diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands/server_list.rs similarity index 100% rename from src-tauri/src/commands.rs rename to src-tauri/src/commands/server_list.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d2095bf..7c6aa06 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,8 +5,10 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ - commands::save_server_list, - commands::get_server_list + commands::server_list::save_server_list, + commands::server_list::get_server_list, + commands::accounts::save_accounts, + commands::accounts::get_accounts, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.tsx b/src/App.tsx index 59a9fd5..0331cdc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useRef } from "react"; import Enclave from "@/app/app"; import ServerList from "./components/view/serverList"; +import { NewProfilePage } from "./components/view/NewProfileView"; export default function App() { const appRef = useRef(null); @@ -16,7 +17,12 @@ export default function App() { return (
- + {!appRef.current.accounts || + appRef.current.accounts.accounts.length === 0 ? ( + + ) : ( + + )}
); } diff --git a/src/app/app.ts b/src/app/app.ts index 1b341a2..56ea1e9 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -11,6 +11,7 @@ import { sha512 } from "@noble/hashes/sha2.js"; import { base58 } from "@scure/base"; import axios from "axios"; +import { AccountsFile, getAccounts } from "@/lib/accounts"; ed.hashes.sha512 = sha512; @@ -30,6 +31,7 @@ ed.hashes.sha512 = sha512; export default class Enclave { private clientSecretKey: Uint8Array; private clientPublicKey: Uint8Array; + public accounts?: AccountsFile; public server?: EnclaveServer; public serverList: ServerList; @@ -41,6 +43,7 @@ export default class Enclave { public async init() { this.serverList = await getServerList(); + this.accounts = await getAccounts(); } public async connectToServer(hostname: string, isSecure: boolean) { diff --git a/src/components/view/NewProfileView.tsx b/src/components/view/NewProfileView.tsx new file mode 100644 index 0000000..49dba06 --- /dev/null +++ b/src/components/view/NewProfileView.tsx @@ -0,0 +1,89 @@ +import * as ed from "@noble/ed25519"; +import { base58 } from "@scure/base"; + +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { saveAccounts } from "@/lib/accounts"; +import Enclave from "@/app/app"; + +export function NewProfilePage({ + appRef, +}: { + appRef: React.RefObject; +}) { + const [displayName, setDisplayName] = useState(""); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + async function handleCreate() { + const name = displayName.trim(); + if (!name) { + setError("Display name is required"); + return; + } + + setSubmitting(true); + setError(null); + + try { + if (!appRef.current) { + console.error("AppRef is not initialized yet"); + return; + } + + if (!appRef.current.accounts) { + appRef.current.accounts = { activeAccount: 0, accounts: [] }; + } + + const { secretKey } = ed.keygen(); + + const newAccount = { + displayName: name, + privateKey: base58.encode(secretKey), + }; + + appRef.current.accounts.activeAccount = + appRef.current.accounts.accounts.length; + + appRef.current.accounts.accounts.push(newAccount); + + saveAccounts(appRef.current.accounts); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to create profile"); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+

Create your profile

+

+ Enclave generates a keypair for your identity — no email or password + required. +

+
+ +
+ + setDisplayName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleCreate()} + /> + {error &&

{error}

} +
+ + +
+
+ ); +} diff --git a/src/lib/accounts.ts b/src/lib/accounts.ts new file mode 100644 index 0000000..b6496e2 --- /dev/null +++ b/src/lib/accounts.ts @@ -0,0 +1,19 @@ +import { invoke } from "@tauri-apps/api/core"; + +export interface Account { + displayName: string; + privateKey: string; +} + +export interface AccountsFile { + activeAccount: number | null; + accounts: Account[]; +} + +export async function getAccounts(): Promise { + return await invoke("get_accounts"); +} + +export async function saveAccounts(data: AccountsFile): Promise { + await invoke("save_accounts", { data }); +}