aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-05-30 12:49:22 +0100
committerGitHub <noreply@github.com>2024-05-30 12:49:22 +0100
commit467f89c104df40904ef4c6b408507e90fe661724 (patch)
treee93697bdfa14ca6b083b0ea02c85d1d0688e0eba /ui
parentchore(deps): bump rusty_paseto and rusty_paserk (#2054) (diff)
downloadatuin-467f89c104df40904ef4c6b408507e90fe661724.zip
feat(ui): add login/register dialog (#2056)
Diffstat (limited to 'ui')
-rw-r--r--ui/backend/Cargo.lock147
-rw-r--r--ui/backend/src/main.rs44
-rw-r--r--ui/package.json1
-rw-r--r--ui/pnpm-lock.yaml3
-rw-r--r--ui/src/App.tsx39
-rw-r--r--ui/src/components/Button.tsx20
-rw-r--r--ui/src/components/LoginOrRegister.tsx341
-rw-r--r--ui/src/components/history/Stats.tsx9
-rw-r--r--ui/src/components/ui/dialog.tsx120
-rw-r--r--ui/src/state/client.ts16
-rw-r--r--ui/src/state/models.ts16
-rw-r--r--ui/src/state/store.ts3
12 files changed, 655 insertions, 104 deletions
diff --git a/ui/backend/Cargo.lock b/ui/backend/Cargo.lock
index b6eaa4b3..58fd5b4f 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",
@@ -348,12 +349,6 @@ dependencies = [
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
@@ -402,22 +397,11 @@ dependencies = [
[[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]]
@@ -603,23 +587,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
- "cipher 0.4.4",
+ "cipher",
"cpufeatures",
]
@@ -638,15 +611,6 @@ dependencies = [
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
@@ -935,23 +899,13 @@ dependencies = [
]
[[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"
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",
@@ -1101,15 +1055,6 @@ dependencies = [
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
@@ -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]]
@@ -3148,6 +3093,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -3826,6 +3780,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -5281,6 +5243,25 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/ui/backend/src/main.rs b/ui/backend/src/main.rs
index 62f833f3..88c9ae87 100644
--- a/ui/backend/src/main.rs
+++ b/ui/backend/src/main.rs
@@ -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,
diff --git a/ui/package.json b/ui/package.json
index a761dccf..f949c0d3 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 d6a2be17..0319a0e2 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 ae6ebdb1..54b62c46 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() {
</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>
diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx
new file mode 100644
index 00000000..5f7e1160
--- /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 (
+ <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>
+ );
+}
diff --git a/ui/src/components/LoginOrRegister.tsx b/ui/src/components/LoginOrRegister.tsx
new file mode 100644
index 00000000..c13c314c
--- /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<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)} />;
+}
diff --git a/ui/src/components/history/Stats.tsx b/ui/src/components/history/Stats.tsx
index 9e2c9a64..f399eaf0 100644
--- a/ui/src/components/history/Stats.tsx
+++ b/ui/src/components/history/Stats.tsx
@@ -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>
);
}
diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx
new file mode 100644
index 00000000..c23630eb
--- /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<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,
+}
diff --git a/ui/src/state/client.ts b/ui/src/state/client.ts
index f43683c1..5ec0d8a7 100644
--- a/ui/src/state/client.ts
+++ b/ui/src/state/client.ts
@@ -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 });
+}
diff --git a/ui/src/state/models.ts b/ui/src/state/models.ts
index 57db44ae..c1d97f4b 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 5e2570bb..6746c1fb 100644
--- a/ui/src/state/store.ts
+++ b/ui/src/state/store.ts
@@ -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) => {