diff --git a/Cargo.lock b/Cargo.lock index fd288da724b..cf60b4e6a82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,7 @@ dependencies = [ "sqlx", "thiserror", "time", + "tiny-bip39", "tokio", "typed-builder", "urlencoding", diff --git a/crates/atuin-client/Cargo.toml b/crates/atuin-client/Cargo.toml index fcff56a3676..5b1ae78b016 100644 --- a/crates/atuin-client/Cargo.toml +++ b/crates/atuin-client/Cargo.toml @@ -68,6 +68,7 @@ reqwest = { workspace = true, optional = true } hex = { version = "0.4", optional = true } sha2 = { version = "0.10", optional = true } indicatif = "0.17.7" +tiny-bip39 = "1" [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/crates/atuin-client/src/lib.rs b/crates/atuin-client/src/lib.rs index 66258af3d0f..89e2dca5687 100644 --- a/crates/atuin-client/src/lib.rs +++ b/crates/atuin-client/src/lib.rs @@ -13,8 +13,10 @@ pub mod encryption; pub mod history; pub mod import; pub mod kv; +pub mod login; pub mod ordering; pub mod record; +pub mod register; pub mod secrets; pub mod settings; diff --git a/crates/atuin-client/src/login.rs b/crates/atuin-client/src/login.rs new file mode 100644 index 00000000000..0530346401f --- /dev/null +++ b/crates/atuin-client/src/login.rs @@ -0,0 +1,91 @@ +use std::path::PathBuf; + +use atuin_common::api::LoginRequest; +use eyre::{bail, Context, Result}; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; + +use crate::{ + api_client, + encryption::{decode_key, encode_key, load_key, Key}, + record::{sqlite_store::SqliteStore, store::Store}, + settings::Settings, +}; + +pub async fn login( + settings: &Settings, + store: &SqliteStore, + username: String, + password: String, + key: String, +) -> Result { + // try parse the key as a mnemonic... + let key = match bip39::Mnemonic::from_phrase(&key, bip39::Language::English) { + Ok(mnemonic) => encode_key(Key::from_slice(mnemonic.entropy()))?, + Err(err) => { + if let Some(err) = err.downcast_ref::() { + match err { + // assume they copied in the base64 key + bip39::ErrorKind::InvalidWord => key, + bip39::ErrorKind::InvalidChecksum => { + bail!("key mnemonic was not valid") + } + bip39::ErrorKind::InvalidKeysize(_) + | bip39::ErrorKind::InvalidWordLength(_) + | bip39::ErrorKind::InvalidEntropyLength(_, _) => { + bail!("key was not the correct length") + } + } + } else { + // unknown error. assume they copied the base64 key + key + } + } + }; + + let key_path = settings.key_path.as_str(); + let key_path = PathBuf::from(key_path); + + if !key_path.exists() { + if decode_key(key.clone()).is_err() { + bail!("the specified key was invalid"); + } + + let mut file = File::create(key_path).await?; + file.write_all(key.as_bytes()).await?; + } else { + // we now know that the user has logged in specifying a key, AND that the key path + // exists + + // 1. check if the saved key and the provided key match. if so, nothing to do. + // 2. if not, re-encrypt the local history and overwrite the key + let current_key: [u8; 32] = load_key(settings)?.into(); + + let encoded = key.clone(); // gonna want to save it in a bit + let new_key: [u8; 32] = decode_key(key) + .context("could not decode provided key - is not valid base64")? + .into(); + + if new_key != current_key { + println!("\nRe-encrypting local store with new key"); + + store.re_encrypt(¤t_key, &new_key).await?; + + println!("Writing new key"); + let mut file = File::create(key_path).await?; + file.write_all(encoded.as_bytes()).await?; + } + } + + let session = api_client::login( + settings.sync_address.as_str(), + LoginRequest { username, password }, + ) + .await?; + + let session_path = settings.session_path.as_str(); + let mut file = File::create(session_path).await?; + file.write_all(session.session.as_bytes()).await?; + + Ok(session.session) +} diff --git a/crates/atuin-client/src/register.rs b/crates/atuin-client/src/register.rs new file mode 100644 index 00000000000..dae01efd16d --- /dev/null +++ b/crates/atuin-client/src/register.rs @@ -0,0 +1,23 @@ +use eyre::Result; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; + +use crate::{api_client, settings::Settings}; + +pub async fn register( + settings: &Settings, + username: String, + email: String, + password: String, +) -> Result { + let session = + api_client::register(settings.sync_address.as_str(), &username, &email, &password).await?; + + let path = settings.session_path.as_str(); + let mut file = File::create(path).await?; + file.write_all(session.session.as_bytes()).await?; + + let _key = crate::encryption::load_key(settings)?; + + Ok(session.session) +} diff --git a/crates/atuin/src/command/client/account/login.rs b/crates/atuin/src/command/client/account/login.rs index 6b72cc63f24..a1b95ad9dca 100644 --- a/crates/atuin/src/command/client/account/login.rs +++ b/crates/atuin/src/command/client/account/login.rs @@ -35,6 +35,12 @@ fn get_input() -> Result { impl Cmd { pub async fn run(&self, settings: &Settings, store: &SqliteStore) -> Result<()> { + // TODO(ellie): Replace this with a call to atuin_client::login::login + // The reason I haven't done this yet is that this implementation allows for + // an empty key. This will use an existing key file. + // + // I'd quite like to ditch that behaviour, so have not brought it into the library + // function. if settings.logged_in() { println!( "You are already logged in! Please run 'atuin logout' if you wish to login again" diff --git a/ui/backend/Cargo.lock b/ui/backend/Cargo.lock index b6eaa4b336f..58fd5b4fe3f 100644 --- a/ui/backend/Cargo.lock +++ b/ui/backend/Cargo.lock @@ -147,7 +147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", - "blake2 0.10.6", + "blake2", "cpufeatures", "password-hash", ] @@ -257,6 +257,7 @@ dependencies = [ "sqlx", "thiserror", "time", + "tiny-bip39", "tokio", "typed-builder", "urlencoding", @@ -346,12 +347,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -400,24 +395,13 @@ dependencies = [ "serde", ] -[[package]] -name = "blake2" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" -dependencies = [ - "crypto-mac", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -601,17 +585,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e53693616d3075149f4ead59bdeecd204ac6b8192d8969757601b74bddf00f" -[[package]] -name = "chacha20" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" -dependencies = [ - "cfg-if", - "cipher 0.3.0", - "cpufeatures", -] - [[package]] name = "chacha20" version = "0.9.1" @@ -619,7 +592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] @@ -636,15 +609,6 @@ dependencies = [ "windows-targets 0.52.5", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -934,16 +898,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "crypto_secretbox" version = "0.1.1" @@ -951,7 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" dependencies = [ "aead", - "cipher 0.4.4", + "cipher", "generic-array", "poly1305", "salsa20", @@ -1005,7 +959,7 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest 0.10.7", + "digest", "fiat-crypto", "platforms", "rustc_version", @@ -1099,15 +1053,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" @@ -2006,7 +1951,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -2336,9 +2281,9 @@ checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "iso8601" -version = "0.4.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b94fbeb759754d87e1daea745bc8efd3037cd16980331fe1d1524c9a79ce96" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" dependencies = [ "nom", ] @@ -2671,7 +2616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest 0.10.7", + "digest", ] [[package]] @@ -3147,6 +3092,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3806,7 +3760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", - "digest 0.10.7", + "digest", "num-bigint-dig", "num-integer", "num-traits", @@ -3825,6 +3779,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -3939,18 +3899,18 @@ checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "rusty_paserk" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d471e07f9e792e60a4d90af4657e2d07e30f20156b689fe5bc1577ecb2e0ec" +checksum = "a1d3a81ecd341ee8abf4761350ecffe3518c284ed1626bbd58f5f4bd64c61d38" dependencies = [ "argon2", - "base64 0.13.1", + "base64 0.22.1", "base64ct", - "blake2 0.10.6", - "chacha20 0.9.1", - "cipher 0.4.4", + "blake2", + "chacha20", + "cipher", "curve25519-dalek", - "digest 0.10.7", + "digest", "ed25519-dalek", "generic-array", "rand 0.8.5", @@ -3962,16 +3922,18 @@ dependencies = [ [[package]] name = "rusty_paseto" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aadb59ff4f705031fae18f6a0261dae6869f70cfd5d134eac497d3841cc3644" +checksum = "b03abd0624688047cc65eadc5588dd35be44151ce7b6e7e7dea4976f5b3dcd54" dependencies = [ - "base64 0.13.1", - "blake2 0.9.2", - "chacha20 0.8.2", + "base64 0.22.1", + "blake2", + "chacha20", + "digest", "ed25519-dalek", "hex", "iso8601", + "rand_core 0.6.4", "ring", "thiserror", "time", @@ -3990,7 +3952,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher 0.4.4", + "cipher", ] [[package]] @@ -4265,7 +4227,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -4276,7 +4238,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -4333,7 +4295,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] @@ -4578,7 +4540,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest 0.10.7", + "digest", "dotenvy", "either", "futures-channel", @@ -5280,6 +5242,25 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand 0.8.5", + "rustc-hash", + "sha2", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/ui/backend/src/main.rs b/ui/backend/src/main.rs index 62f833f3455..88c9ae8760c 100644 --- a/ui/backend/src/main.rs +++ b/ui/backend/src/main.rs @@ -77,7 +77,41 @@ async fn config() -> Result { #[tauri::command] async fn session() -> Result { - Settings::new().map_err(|e|e.to_string())?.session_token().map_err(|e|e.to_string()) + Settings::new() + .map_err(|e| e.to_string())? + .session_token() + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn login(username: String, password: String, key: String) -> Result { + let settings = Settings::new().map_err(|e| e.to_string())?; + + let record_store_path = PathBuf::from(settings.record_store_path.as_str()); + let store = SqliteStore::new(record_store_path, settings.local_timeout) + .await + .map_err(|e| e.to_string())?; + + if settings.logged_in() { + return Err(String::from("Already logged in")); + } + + let session = atuin_client::login::login(&settings, &store, username, password, key) + .await + .map_err(|e| e.to_string())?; + + Ok(session) +} + +#[tauri::command] +async fn register(username: String, email: String, password: String) -> Result { + let settings = Settings::new().map_err(|e| e.to_string())?; + + let session = atuin_client::register::register(&settings, username, email, password) + .await + .map_err(|e| e.to_string())?; + + Ok(session) } #[tauri::command] @@ -88,7 +122,6 @@ async fn home_info() -> Result { .await .map_err(|e| e.to_string())?; - let last_sync = Settings::last_sync() .map_err(|e| e.to_string())? .format(&Rfc3339) @@ -110,7 +143,10 @@ async fn home_info() -> Result { } else { let client = atuin_client::api_client::Client::new( &settings.sync_address, - settings.session_token().map_err(|e|e.to_string())?.as_str(), + settings + .session_token() + .map_err(|e| e.to_string())? + .as_str(), settings.network_connect_timeout, settings.network_timeout, ) @@ -139,6 +175,8 @@ fn main() { home_info, config, session, + login, + register, dotfiles::aliases::import_aliases, dotfiles::aliases::delete_alias, dotfiles::aliases::set_alias, diff --git a/ui/package.json b/ui/package.json index a761dccfe91..f949c0d3868 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "dependencies": { "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.3", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.0.2", "@tailwindcss/forms": "^0.5.7", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index d6a2be17268..0319a0e20a8 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@heroicons/react': specifier: ^2.1.3 version: 2.1.3(react@18.2.0) + '@radix-ui/react-dialog': + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.24)(@types/react@18.2.74)(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.24)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ae6ebdb1a92..54b62c468a1 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,19 @@ import "./App.css"; import { useState, ReactElement } from "react"; +import { useStore } from "@/state/store"; + +import Button, { ButtonStyle } from "@/components/Button"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + import { Cog6ToothIcon, HomeIcon, @@ -16,6 +29,7 @@ function classNames(...classes: any) { import Home from "./pages/Home.tsx"; import History from "./pages/History.tsx"; import Dotfiles from "./pages/Dotfiles.tsx"; +import LoginOrRegister from "./components/LoginOrRegister.tsx"; enum Section { Home, @@ -39,6 +53,8 @@ function App() { // I think hashrouter may work, but I'd rather avoiding thinking of them as // pages const [section, setSection] = useState(Section.Home); + const user = useStore((state) => state.user); + console.log(user); const navigation = [ { @@ -96,16 +112,19 @@ function App() {
  • - - + {user && !user.isLoggedIn() && ( + + + + )}
  • diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx new file mode 100644 index 00000000000..5f7e11601ac --- /dev/null +++ b/ui/src/components/Button.tsx @@ -0,0 +1,20 @@ +export enum ButtonStyle { + PrimarySm = "bg-emerald-500 hover:bg-emerald-600", + PrimarySmFill = "bg-emerald-500 hover:bg-emerald-600 w-full text-sm", +} + +interface ButtonProps { + text: string; + style: ButtonStyle; +} + +export default function Button(props: ButtonProps) { + return ( + + ); +} diff --git a/ui/src/components/LoginOrRegister.tsx b/ui/src/components/LoginOrRegister.tsx new file mode 100644 index 00000000000..c13c314c5f0 --- /dev/null +++ b/ui/src/components/LoginOrRegister.tsx @@ -0,0 +1,341 @@ +import Logo from "@/assets/logo-light.svg"; +import { useState } from "react"; + +import { login, register } from "@/state/client"; +import { useStore } from "@/state/store"; + +interface LoginProps { + toggleRegister: () => void; +} + +function Login(props: LoginProps) { + const refreshUser = useStore((state) => state.refreshUser); + const [errors, setErrors] = useState(null); + + const doLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + const form = e.currentTarget; + const username = form.username.value; + const password = form.password.value; + const key = form.key.value; + + console.log("Logging in..."); + try { + await login(username, password, key); + refreshUser(); + console.log("Logged in"); + } catch (e) { + console.error(e); + setErrors(e); + } + }; + + return ( + <> +
    +
    + Atuin + +

    + Sign in to your account +

    + +

    + Backup and sync your data across devices. All data is end-to-end + encrypted and stored securely in the cloud. +

    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
    + {/* You can't right now. Sorry. Validate emails first. + + Forgot password? + + */} +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    + + {errors && ( +

    {errors}

    + )} + +

    + Not a member?{" "} + { + e.preventDefault(); + props.toggleRegister(); + }} + > + Register + +

    +
    +
    + + ); +} + +interface RegisterProps { + toggleLogin: () => void; +} + +function Register(props: RegisterProps) { + const refreshUser = useStore((state) => state.refreshUser); + const [errors, setErrors] = useState(null); + + const doRegister = async (e: React.FormEvent) => { + e.preventDefault(); + + const form = e.currentTarget; + const username = form.username.value; + const email = form.email.value; + const password = form.password.value; + + console.log("Logging in..."); + try { + await register(username, email, password); + refreshUser(); + console.log("Logged in"); + } catch (e) { + console.error(e); + setErrors(e); + } + }; + + return ( + <> +
    +
    + Atuin + +

    + Register for an account +

    + +

    + Backup and sync your data across devices. All data is end-to-end + encrypted and stored securely in the cloud. +

    +
    + +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    + {/* You can't right now. Sorry. Validate emails first. + + Forgot password? + + */} +
    +
    +
    + +
    +
    + +
    + +
    +
    + + {errors && ( +

    {errors}

    + )} + +

    + Already have an account?{" "} + { + e.preventDefault(); + props.toggleLogin(); + }} + > + Login + +

    +
    +
    + + ); +} + +export default function LoginOrRegister() { + let [login, setLogin] = useState(false); + + if (login) { + return setLogin(false)} />; + } + + return setLogin(true)} />; +} diff --git a/ui/src/components/history/Stats.tsx b/ui/src/components/history/Stats.tsx index 9e2c9a64f3f..f399eaf0f71 100644 --- a/ui/src/components/history/Stats.tsx +++ b/ui/src/components/history/Stats.tsx @@ -13,8 +13,13 @@ import { function renderLoading() { return ( -
    - +
    +
    + +
    +
    +

    Crunching the latest numbers...

    +
    ); } diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx new file mode 100644 index 00000000000..c23630eb841 --- /dev/null +++ b/ui/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/ui/src/state/client.ts b/ui/src/state/client.ts index f43683c1ff3..5ec0d8a7e4b 100644 --- a/ui/src/state/client.ts +++ b/ui/src/state/client.ts @@ -11,3 +11,19 @@ export async function sessionToken(): Promise { export async function settings(): Promise { return await invoke("config"); } + +export async function login( + username: string, + password: string, + key: string, +): Promise { + return await invoke("login", { username, password, key }); +} + +export async function register( + username: string, + email: string, + password: string, +): Promise { + return await invoke("register", { username, email, password }); +} diff --git a/ui/src/state/models.ts b/ui/src/state/models.ts index 57db44ae58c..c1d97f4bbbc 100644 --- a/ui/src/state/models.ts +++ b/ui/src/state/models.ts @@ -1,12 +1,18 @@ import Database from "@tauri-apps/plugin-sql"; -export interface User { - username: string; +export class User { + username: string | null; + + constructor(username: string) { + this.username = username; + } + + isLoggedIn(): boolean { + return this.username !== "" && this.username !== null; + } } -export const DefaultUser: User = { - username: "", -}; +export const DefaultUser: User = new User(""); export interface HomeInfo { historyCount: number; diff --git a/ui/src/state/store.ts b/ui/src/state/store.ts index 5e2570bb5c9..6746c1fb4c3 100644 --- a/ui/src/state/store.ts +++ b/ui/src/state/store.ts @@ -94,6 +94,7 @@ export const useStore = create()((set, get) => ({ session = await sessionToken(); } catch (e) { console.log("Not logged in, so not refreshing user"); + set({ user: DefaultUser }); return; } let url = config.sync_address + "/api/v0/me"; @@ -105,7 +106,7 @@ export const useStore = create()((set, get) => ({ }); let me = await res.json(); - set({ user: me }); + set({ user: new User(me.username) }); }, historyNextPage: (query?: string) => {