mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-25 01:34:13 +01:00
feat(ui): add login/register dialog (#2056)
This commit is contained in:
parent
15618f19ab
commit
467f89c104
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -292,6 +292,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tiny-bip39",
|
||||
"tokio",
|
||||
"typed-builder",
|
||||
"urlencoding",
|
||||
|
@ -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"] }
|
||||
|
@ -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;
|
||||
|
||||
|
91
crates/atuin-client/src/login.rs
Normal file
91
crates/atuin-client/src/login.rs
Normal file
@ -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<String> {
|
||||
// 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::<bip39::ErrorKind>() {
|
||||
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)
|
||||
}
|
23
crates/atuin-client/src/register.rs
Normal file
23
crates/atuin-client/src/register.rs
Normal file
@ -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<String> {
|
||||
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)
|
||||
}
|
@ -35,6 +35,12 @@ fn get_input() -> Result<String> {
|
||||
|
||||
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"
|
||||
|
147
ui/backend/Cargo.lock
generated
147
ui/backend/Cargo.lock
generated
@ -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"
|
||||
|
@ -77,7 +77,41 @@ async fn config() -> Result<Settings, String> {
|
||||
|
||||
#[tauri::command]
|
||||
async fn session() -> Result<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<HomeInfo, String> {
|
||||
.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<HomeInfo, String> {
|
||||
} 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,
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
</ul>
|
||||
</li>
|
||||
<li className="mt-auto">
|
||||
<a
|
||||
href="#"
|
||||
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-green-600"
|
||||
>
|
||||
<Cog6ToothIcon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-green-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Settings
|
||||
</a>
|
||||
{user && !user.isLoggedIn() && (
|
||||
<Dialog>
|
||||
<DialogTrigger className="w-full">
|
||||
<Button
|
||||
text={"Login or Register"}
|
||||
style={ButtonStyle.PrimarySmFill}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<LoginOrRegister />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
20
ui/src/components/Button.tsx
Normal file
20
ui/src/components/Button.tsx
Normal file
@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded ${props.style} px-2 py-1 font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500`}
|
||||
>
|
||||
{props.text}
|
||||
</button>
|
||||
);
|
||||
}
|
341
ui/src/components/LoginOrRegister.tsx
Normal file
341
ui/src/components/LoginOrRegister.tsx
Normal file
@ -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<string | null>(null);
|
||||
|
||||
const doLogin = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center px-6 ">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img className="mx-auto h-10 w-auto" src={Logo} alt="Atuin" />
|
||||
|
||||
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-center text-gray-600 mt-4 text-wrap">
|
||||
Backup and sync your data across devices. All data is end-to-end
|
||||
encrypted and stored securely in the cloud.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form
|
||||
className="space-y-6"
|
||||
action="#"
|
||||
method="POST"
|
||||
onSubmit={doLogin}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="username"
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
required
|
||||
className="block w-full rounded-md border-0 px-1.5 py-1.5 outline-none text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div className="text-sm">
|
||||
{/* You can't right now. Sorry. Validate emails first.
|
||||
<a
|
||||
href="#"
|
||||
className="font-semibold text-emerald-600 hover:text-emerald-500"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-emerald-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="key"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
<p>Key</p>
|
||||
<p className="text-xs text-gray-500 font-normal">
|
||||
Paste the output of "atuin key" from another machine
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="key"
|
||||
name="key"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
required
|
||||
className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-emerald-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full justify-center rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-emerald-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{errors && (
|
||||
<p className="mt-4 text-center text-sm text-red-500">{errors}</p>
|
||||
)}
|
||||
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
Not a member?{" "}
|
||||
<a
|
||||
href="#"
|
||||
className="font-semibold leading-6 text-emerald-600 hover:text-emerald-500"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.toggleRegister();
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface RegisterProps {
|
||||
toggleLogin: () => void;
|
||||
}
|
||||
|
||||
function Register(props: RegisterProps) {
|
||||
const refreshUser = useStore((state) => state.refreshUser);
|
||||
const [errors, setErrors] = useState<string | null>(null);
|
||||
|
||||
const doRegister = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center px-6 ">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<img className="mx-auto h-10 w-auto" src={Logo} alt="Atuin" />
|
||||
|
||||
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
Register for an account
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-center text-gray-600 mt-4 text-wrap">
|
||||
Backup and sync your data across devices. All data is end-to-end
|
||||
encrypted and stored securely in the cloud.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
<form
|
||||
className="space-y-6"
|
||||
action="#"
|
||||
method="POST"
|
||||
onSubmit={doRegister}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="username"
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
required
|
||||
className="block w-full rounded-md border-0 px-1.5 py-1.5 outline-none text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
required
|
||||
className="block w-full rounded-md border-0 px-1.5 py-1.5 outline-none text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium leading-6 text-gray-900"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div className="text-sm">
|
||||
{/* You can't right now. Sorry. Validate emails first.
|
||||
<a
|
||||
href="#"
|
||||
className="font-semibold text-emerald-600 hover:text-emerald-500"
|
||||
>
|
||||
Forgot password?
|
||||
</a>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="block w-full rounded-md border-0 px-1.5 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-emerald-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full justify-center rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-emerald-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600"
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{errors && (
|
||||
<p className="mt-4 text-center text-sm text-red-500">{errors}</p>
|
||||
)}
|
||||
|
||||
<p className="mt-10 text-center text-sm text-gray-500">
|
||||
Already have an account?{" "}
|
||||
<a
|
||||
href="#"
|
||||
className="font-semibold leading-6 text-emerald-600 hover:text-emerald-500"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.toggleLogin();
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginOrRegister() {
|
||||
let [login, setLogin] = useState<boolean>(false);
|
||||
|
||||
if (login) {
|
||||
return <Login toggleRegister={() => setLogin(false)} />;
|
||||
}
|
||||
|
||||
return <Register toggleLogin={() => setLogin(true)} />;
|
||||
}
|
@ -13,8 +13,13 @@ import {
|
||||
|
||||
function renderLoading() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<PacmanLoader color="#26bd65" />
|
||||
<div className="flex flex-col items-center justify-center h-full ">
|
||||
<div>
|
||||
<PacmanLoader color="#26bd65" />
|
||||
</div>
|
||||
<div className="block mt-4">
|
||||
<p>Crunching the latest numbers...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
120
ui/src/components/ui/dialog.tsx
Normal file
120
ui/src/components/ui/dialog.tsx
Normal file
@ -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<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
@ -11,3 +11,19 @@ export async function sessionToken(): Promise<String> {
|
||||
export async function settings(): Promise<Settings> {
|
||||
return await invoke("config");
|
||||
}
|
||||
|
||||
export async function login(
|
||||
username: string,
|
||||
password: string,
|
||||
key: string,
|
||||
): Promise<string> {
|
||||
return await invoke("login", { username, password, key });
|
||||
}
|
||||
|
||||
export async function register(
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<string> {
|
||||
return await invoke("register", { username, email, password });
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -94,6 +94,7 @@ export const useStore = create<AtuinState>()((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<AtuinState>()((set, get) => ({
|
||||
});
|
||||
let me = await res.json();
|
||||
|
||||
set({ user: me });
|
||||
set({ user: new User(me.username) });
|
||||
},
|
||||
|
||||
historyNextPage: (query?: string) => {
|
||||
|
Loading…
Reference in New Issue
Block a user