From f0915b452d1c70983cb024b7701a89a438aaf526 Mon Sep 17 00:00:00 2001 From: Atheer Date: Sun, 11 Aug 2024 22:29:55 +0200 Subject: [PATCH] feat: Client implementation (#2) * feat: initial Tui application setup the event handler is not good and needs to be reworked, this setup is a good starting point * chore: update workspace cargo files * fix: proper program terminattion * refactor: implement default for App * refactor: rewrite event handler * refactor: update main to handle new events the events are tick and mouse events * chore: update cargo.toml * feat: add home and footer components * feat: add new events to handle * chore: add components to lib * feat: add home and footer component to app * feat: update ui to render home and footer * chore: update root cargo.lock * chore: update cargo.toml & .lock * feat: add login popup * feat: update event handling for when selecting an action in home page * feat: refactor ui into seperate package * refactor: update app struct with lifetime * feat: initial poc implementation for login pop * feat: add helper text for login popup * chore: update cargo * feat: add Error mode in app.rs * feat: update event handler to handle Error event * chore: update text in footer * feat: add validation of login credentials and error popup * feat: handle error event in program loop * feat: add validation & helper text for register * feat: add login functionality * feat: add register functionality * chore: fix small clippy suggestions * feat: initial PoC implementation for receiving and sending messages * feat: improve sending and receiving message the sending and receving of message have improved since we no longer require to clone the app each time, now each api for the client is created separately in the client.rs * feat: improve chat functionality in client * feat: add error popup when client want to send empty message * refactor: update chatapi to receive a message without having to send something first * fix: correctly value of password check operation * chore: remove unecessary prints * fix: update handling of expired jwts * chore: fix formatting * chore: remove --check for sqlx prepare git workflow * fix: add cargo update in github workflow * chore: update cargo dependencies to latest version --- .github/workflows/general.yml | 4 +- Cargo.lock | 1183 ++++++++++++----- Cargo.toml | 2 +- auth/src/authentication.rs | 102 +- auth/src/lib.rs | 1 + .../auth_service/auth_token/generation.rs | 8 - .../src/server/auth_service/auth_token/mod.rs | 2 + .../auth_token/postgres_operations.rs | 25 + .../auth_token/redis_operations.rs | 10 + .../auth_service/auth_token/validation.rs | 21 + .../auth_service/check_existing_user.rs | 21 +- auth/src/server/auth_service/mod.rs | 66 +- auth/src/server/auth_service/register.rs | 21 +- chat/src/chat.rs | 66 +- chat/src/lib.rs | 1 + chat/src/server/auth_interceptor.rs | 9 +- client/Cargo.toml | 30 + client/src/api/auth.rs | 46 + client/src/api/chat.rs | 74 ++ client/src/api/mod.rs | 5 + client/src/app.rs | 55 + client/src/bin/client.rs | 102 ++ client/src/components/footer.rs | 62 + client/src/components/home/chat/mod.rs | 154 +++ client/src/components/home/login/mod.rs | 235 ++++ client/src/components/home/mod.rs | 133 ++ client/src/components/home/register/mod.rs | 278 ++++ client/src/components/home/validation.rs | 98 ++ client/src/components/mod.rs | 5 + client/src/events/event.rs | 145 ++ client/src/events/event_handler.rs | 81 ++ client/src/events/mod.rs | 7 + client/src/lib.rs | 6 + client/src/tui.rs | 57 + client/src/ui/mod.rs | 27 + client/src/ui/utils.rs | 17 + 36 files changed, 2634 insertions(+), 525 deletions(-) create mode 100644 auth/src/server/auth_service/auth_token/validation.rs create mode 100644 client/Cargo.toml create mode 100644 client/src/api/auth.rs create mode 100644 client/src/api/chat.rs create mode 100644 client/src/api/mod.rs create mode 100644 client/src/app.rs create mode 100644 client/src/bin/client.rs create mode 100644 client/src/components/footer.rs create mode 100644 client/src/components/home/chat/mod.rs create mode 100644 client/src/components/home/login/mod.rs create mode 100644 client/src/components/home/mod.rs create mode 100644 client/src/components/home/register/mod.rs create mode 100644 client/src/components/home/validation.rs create mode 100644 client/src/components/mod.rs create mode 100644 client/src/events/event.rs create mode 100644 client/src/events/event_handler.rs create mode 100644 client/src/events/mod.rs create mode 100644 client/src/lib.rs create mode 100644 client/src/tui.rs create mode 100644 client/src/ui/mod.rs create mode 100644 client/src/ui/utils.rs diff --git a/.github/workflows/general.yml b/.github/workflows/general.yml index c24f6e9..7af50b0 100644 --- a/.github/workflows/general.yml +++ b/.github/workflows/general.yml @@ -44,6 +44,8 @@ jobs: with: # this key will be added to tha automatic 'job'-based cache key, this key can also be used in further different jobs key: "sqlx-${{ env.SQLX_VERSION }}" + - name: update cargo + run: cargo update - name: Install sqlx cli run: cargo install sqlx-cli --version=${{ env.SQLX_VERSION }} @@ -58,7 +60,7 @@ jobs: SKIP_DOCKER=true auth/scripts/init_db.sh - name: Check sqlx-data.json is up-to-date run: | - cargo sqlx prepare --workspace --check + cargo sqlx prepare --workspace - name: Run tests run: cargo test -- --test-threads=1 diff --git a/Cargo.lock b/Cargo.lock index 7250fcb..a109c56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -32,18 +32,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -62,15 +62,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "argon2" @@ -103,18 +103,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] name = "async-trait" -version = "0.1.75" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] @@ -126,16 +126,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-write-file" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" -dependencies = [ - "nix", - "rand", -] - [[package]] name = "auth" version = "0.1.0" @@ -176,9 +166,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -227,9 +217,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -248,9 +238,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -266,9 +256,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -305,19 +295,31 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] -name = "cc" -version = "1.0.83" +name = "cassowary" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ - "libc", + "rustversion", ] +[[package]] +name = "cc" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" + [[package]] name = "cfg-if" version = "1.0.0" @@ -361,7 +363,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -373,11 +375,34 @@ dependencies = [ "autocfg", ] +[[package]] +name = "client" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "auth", + "chat", + "crossterm", + "futures", + "futures-timer", + "random_color", + "ratatui", + "tokio", + "tokio-stream", + "tonic", + "tui-big-text", + "tui-popup", + "tui-prompts", + "unicode-segmentation", + "validator", +] + [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "futures-core", @@ -387,6 +412,20 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "config" version = "0.13.4" @@ -409,9 +448,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -424,9 +463,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -439,21 +478,43 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-queue" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "cfg-if", + "bitflags 2.6.0", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", ] [[package]] @@ -466,11 +527,46 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.74", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.74", +] + [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -479,18 +575,72 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.74", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core", + "syn 2.0.74", +] + +[[package]] +name = "derive_setters" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "deunicode" -version = "1.4.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" [[package]] name = "digest" @@ -504,6 +654,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -512,9 +671,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] @@ -537,9 +696,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -574,15 +733,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - -[[package]] -name = "finl_unicode" -version = "1.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fixedbitset" @@ -598,7 +751,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -607,6 +760,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "font8x8" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -683,7 +842,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] @@ -698,6 +857,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" @@ -738,9 +903,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -749,15 +914,21 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -765,7 +936,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -780,9 +951,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -794,7 +965,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -806,11 +977,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -847,9 +1024,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -869,9 +1046,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -881,9 +1058,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -938,6 +1115,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -970,37 +1153,47 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", +] + +[[package]] +name = "instability" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +dependencies = [ + "quote", + "syn 2.0.74", ] [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -1028,18 +1221,18 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] name = "libc" -version = "0.2.151" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -1066,15 +1259,21 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litrs" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1082,9 +1281,18 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "matchers" @@ -1113,9 +1321,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1131,40 +1339,31 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.10" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ + "hermit-abi", "libc", + "log", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.4.1", - "cfg-if", - "libc", -] +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "nom" @@ -1203,21 +1402,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1226,29 +1430,19 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -1267,9 +1461,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1277,15 +1471,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -1301,9 +1495,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -1328,39 +1522,39 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.1.0", + "indexmap 2.3.0", ] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1391,9 +1585,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "powerfmt" @@ -1403,34 +1597,46 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "prettyplease" -version = "0.2.15" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.48", + "syn 2.0.74", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -1438,13 +1644,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck", - "itertools 0.11.0", + "heck 0.5.0", + "itertools 0.12.1", "log", "multimap", "once_cell", @@ -1453,29 +1659,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.48", + "syn 2.0.74", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] @@ -1504,9 +1709,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1541,11 +1746,50 @@ dependencies = [ "getrandom", ] +[[package]] +name = "random_color" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0085421bc527effa7ed6d46bac0a28734663c47abe03d80a5e78e441fad85196" +dependencies = [ + "rand", +] + +[[package]] +name = "ratatui" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba6a365afbe5615999275bea2446b970b10a41102500e27ce7678d50d978303" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-macros" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4a88a1767c49fc279878ee4384117a03ae99377b5262da937d66a7d5eea240" +dependencies = [ + "ratatui", +] + [[package]] name = "redis" -version = "0.25.2" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d64e978fd98a0e6b105d066ba4889a7301fca65aeac850a877d8797343feeb" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" dependencies = [ "arc-swap", "async-trait", @@ -1574,16 +1818,25 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" -version = "1.10.2" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1597,13 +1850,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.4", ] [[package]] @@ -1614,22 +1867,29 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1652,19 +1912,58 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.74", + "unicode-ident", +] + [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1673,9 +1972,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "ring", "rustls-webpki", @@ -1688,7 +1987,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] @@ -1703,15 +2002,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" @@ -1739,33 +2038,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" -version = "1.0.193" +version = "1.0.206" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.206" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "a11b3b6ce5cfd25b9759a24c3ed4bf24e23893866863547de4655518c951bcd4" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -1783,9 +2089,9 @@ dependencies = [ [[package]] name = "sha1_smol" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" [[package]] name = "sha2" @@ -1807,11 +2113,32 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -1837,26 +2164,20 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -1878,20 +2199,19 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools 0.12.0", "nom", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1902,9 +2222,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ "ahash", "atoi", @@ -1912,7 +2232,6 @@ dependencies = [ "bytes", "crc", "crossbeam-queue", - "dotenvy", "either", "event-listener", "futures-channel", @@ -1922,7 +2241,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.1.0", + "indexmap 2.3.0", "log", "memchr", "once_cell", @@ -1945,9 +2264,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -1958,14 +2277,13 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ - "atomic-write-file", "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -1985,13 +2303,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.6.0", "byteorder", "bytes", "crc", @@ -2027,13 +2345,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.6.0", "byteorder", "crc", "dotenvy", @@ -2054,7 +2372,6 @@ dependencies = [ "rand", "serde", "serde_json", - "sha1", "sha2", "smallvec", "sqlx-core", @@ -2066,9 +2383,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "flume", @@ -2087,22 +2404,56 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.74", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2117,9 +2468,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -2134,42 +2485,42 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "once_cell", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -2177,12 +2528,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2197,18 +2549,19 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -2221,21 +2574,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2250,13 +2602,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] @@ -2284,16 +2636,32 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.3.0", + "toml_datetime", + "winnow", ] [[package]] @@ -2305,7 +2673,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64 0.21.5", + "base64 0.21.7", "bytes", "h2", "http", @@ -2333,7 +2701,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] @@ -2425,7 +2793,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] @@ -2502,6 +2870,42 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-big-text" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b5f84793dc745875c8f888bc3aa7eaecc1b99cc11425b1d594da88f52eceff0" +dependencies = [ + "derive_builder", + "font8x8", + "itertools 0.13.0", + "ratatui", +] + +[[package]] +name = "tui-popup" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6748b2420d0e846c28103155c8b85de5c0850497f2b0b602894ece9ec9d335" +dependencies = [ + "derive-getters", + "derive_setters", + "document-features", + "ratatui", +] + +[[package]] +name = "tui-prompts" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44db11453dd12271526c3ac5c0f4e7f0456f2e8fc76580c755664655800973c2" +dependencies = [ + "itertools 0.13.0", + "ratatui", + "ratatui-macros", + "rstest", +] + [[package]] name = "typenum" version = "1.17.0" @@ -2510,9 +2914,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -2522,18 +2926,41 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode_categories" @@ -2549,9 +2976,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna 0.5.0", @@ -2566,9 +2993,9 @@ checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "uuid" -version = "1.6.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] @@ -2602,9 +3029,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -2621,6 +3048,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -2642,7 +3075,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", "wasm-bindgen-shared", ] @@ -2664,7 +3097,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2677,28 +3110,20 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] -name = "which" -version = "4.4.2" +name = "whoami" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "either", - "home", - "once_cell", - "rustix", + "redox_syscall 0.4.1", + "wasite", ] -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" - [[package]] name = "winapi" version = "0.3.9" @@ -2727,7 +3152,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -2745,7 +3170,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -2765,17 +3199,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2786,9 +3221,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -2798,9 +3233,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -2810,9 +3245,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -2822,9 +3263,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -2834,9 +3275,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -2846,9 +3287,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -2858,9 +3299,18 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] [[package]] name = "yaml-rust" @@ -2873,26 +3323,27 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.74", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 3653511..4fcde3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["auth", "chat"] +members = ["auth", "chat", "client"] diff --git a/auth/src/authentication.rs b/auth/src/authentication.rs index 418038f..f8bd44c 100644 --- a/auth/src/authentication.rs +++ b/auth/src/authentication.rs @@ -29,8 +29,8 @@ pub struct Token { /// Generated client implementations. pub mod auth_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] - use tonic::codegen::*; use tonic::codegen::http::Uri; + use tonic::codegen::*; #[derive(Debug, Clone)] pub struct AuthClient { inner: tonic::client::Grpc, @@ -61,10 +61,7 @@ pub mod auth_client { let inner = tonic::client::Grpc::with_origin(inner, origin); Self { inner } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> AuthClient> + pub fn with_interceptor(inner: T, interceptor: F) -> AuthClient> where F: tonic::service::Interceptor, T::ResponseBody: Default, @@ -74,9 +71,8 @@ pub mod auth_client { >::ResponseBody, >, >, - , - >>::Error: Into + Send + Sync, + >>::Error: + Into + Send + Sync, { AuthClient::new(InterceptedService::new(inner, interceptor)) } @@ -115,40 +111,31 @@ pub mod auth_client { &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/authentication.Auth/Login", - ); + let path = http::uri::PathAndQuery::from_static("/authentication.Auth/Login"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("authentication.Auth", "Login")); + req.extensions_mut() + .insert(GrpcMethod::new("authentication.Auth", "Login")); self.inner.unary(req, path, codec).await } pub async fn register( &mut self, request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/authentication.Auth/Register", - ); + let path = http::uri::PathAndQuery::from_static("/authentication.Auth/Register"); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("authentication.Auth", "Register")); @@ -195,10 +182,7 @@ pub mod auth_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { @@ -254,21 +238,15 @@ pub mod auth_server { "/authentication.Auth/Login" => { #[allow(non_camel_case_types)] struct LoginSvc(pub Arc); - impl tonic::server::UnaryService - for LoginSvc { + impl tonic::server::UnaryService for LoginSvc { type Response = super::Token; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::login(&inner, request).await - }; + let fut = async move { ::login(&inner, request).await }; Box::pin(fut) } } @@ -298,21 +276,15 @@ pub mod auth_server { "/authentication.Auth/Register" => { #[allow(non_camel_case_types)] struct RegisterSvc(pub Arc); - impl tonic::server::UnaryService - for RegisterSvc { + impl tonic::server::UnaryService for RegisterSvc { type Response = super::Token; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::register(&inner, request).await - }; + let fut = async move { ::register(&inner, request).await }; Box::pin(fut) } } @@ -339,18 +311,14 @@ pub mod auth_server { }; Box::pin(fut) } - _ => { - Box::pin(async move { - Ok( - http::Response::builder() - .status(200) - .header("grpc-status", "12") - .header("content-type", "application/grpc") - .body(empty_body()) - .unwrap(), - ) - }) - } + _ => Box::pin(async move { + Ok(http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap()) + }), } } } diff --git a/auth/src/lib.rs b/auth/src/lib.rs index d2b19aa..dee7961 100644 --- a/auth/src/lib.rs +++ b/auth/src/lib.rs @@ -1,3 +1,4 @@ +pub mod authentication; pub mod configuration; pub mod logging; pub mod proto; diff --git a/auth/src/server/auth_service/auth_token/generation.rs b/auth/src/server/auth_service/auth_token/generation.rs index a415881..f7ed7ae 100644 --- a/auth/src/server/auth_service/auth_token/generation.rs +++ b/auth/src/server/auth_service/auth_token/generation.rs @@ -5,17 +5,11 @@ use jwt::SignWithKey; use secrecy::{ExposeSecret, Secret}; use sha2::Sha512; use std::collections::BTreeMap; -use std::sync::Arc; - -use crate::server::RegisterData; pub fn generate_auth_token( secret_key: Secret, user_id: &str, - register_request_arc: Arc, ) -> Result { - let register_request = register_request_arc.clone(); - let key: Hmac = Hmac::new_from_slice(secret_key.expose_secret().as_bytes())?; let mut claims = BTreeMap::new(); @@ -33,8 +27,6 @@ pub fn generate_auth_token( // application claims claims.insert("user_id", user_id); - claims.insert("username", register_request.username.as_ref()); - claims.insert("email", register_request.email.as_ref()); let token = claims.sign_with_key(&key)?; Ok(token) diff --git a/auth/src/server/auth_service/auth_token/mod.rs b/auth/src/server/auth_service/auth_token/mod.rs index b68b7f1..0fd1dd6 100644 --- a/auth/src/server/auth_service/auth_token/mod.rs +++ b/auth/src/server/auth_service/auth_token/mod.rs @@ -1,7 +1,9 @@ mod generation; mod postgres_operations; mod redis_operations; +mod validation; pub use generation::*; pub use postgres_operations::*; pub use redis_operations::*; +pub use validation::*; diff --git a/auth/src/server/auth_service/auth_token/postgres_operations.rs b/auth/src/server/auth_service/auth_token/postgres_operations.rs index ec9fb24..b172c26 100644 --- a/auth/src/server/auth_service/auth_token/postgres_operations.rs +++ b/auth/src/server/auth_service/auth_token/postgres_operations.rs @@ -26,6 +26,31 @@ pub async fn store_token_db( Ok(()) } +#[tracing::instrument( + name = "update auth_token in DB" + skip(pg_pool, auth_token) +)] +pub async fn update_token_db( + pg_pool: &PgPool, + user_id: &i32, + auth_token: &str, +) -> Result<(), sqlx::Error> { + let query = sqlx::query!( + r#" + UPDATE auth_tokens SET auth_token = $1 WHERE user_id = $2 + "#, + auth_token, + user_id + ); + + pg_pool.execute(query).await.map_err(|e| { + tracing::error!("Failed to exectute query: {:?}", e); + e + })?; + + Ok(()) +} + #[tracing::instrument( name = "get auth_token from db" skip(db_pool, ) diff --git a/auth/src/server/auth_service/auth_token/redis_operations.rs b/auth/src/server/auth_service/auth_token/redis_operations.rs index a399467..81e7d21 100644 --- a/auth/src/server/auth_service/auth_token/redis_operations.rs +++ b/auth/src/server/auth_service/auth_token/redis_operations.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use chrono::{Duration, Local}; use redis::{AsyncCommands, RedisResult}; use crate::server::RedisCon; @@ -20,6 +21,15 @@ pub async fn store_token_redis( return Err(anyhow!("couldn't save auth token in redis")); } + let one_week_from_now_timestamp = (Local::now() + Duration::weeks(1)).timestamp(); + let expire_res: RedisResult<()> = redis_con + .expire_at(user_id, one_week_from_now_timestamp) + .await; + + if expire_res.is_err() { + return Err(anyhow!("couldn't set expire time for auth token in redis")); + } + Ok(()) } diff --git a/auth/src/server/auth_service/auth_token/validation.rs b/auth/src/server/auth_service/auth_token/validation.rs new file mode 100644 index 0000000..9638139 --- /dev/null +++ b/auth/src/server/auth_service/auth_token/validation.rs @@ -0,0 +1,21 @@ +use std::collections::BTreeMap; + +use anyhow::Error; +use chrono::Local; +use hmac::{Hmac, Mac}; +use jwt::VerifyWithKey; +use secrecy::{ExposeSecret, Secret}; +use sha2::Sha512; + +pub fn auth_token_expired(secret_key: Secret, auth_token: &str) -> Result { + let key: Hmac = Hmac::new_from_slice(secret_key.expose_secret().as_bytes())?; + + let claims: BTreeMap = auth_token.verify_with_key(&key).unwrap(); + + let now_timestamp = Local::now().timestamp().to_string(); + if now_timestamp > claims["exp"] { + return Ok(true); + } + + Ok(false) +} diff --git a/auth/src/server/auth_service/check_existing_user.rs b/auth/src/server/auth_service/check_existing_user.rs index 7d4d068..1b03305 100644 --- a/auth/src/server/auth_service/check_existing_user.rs +++ b/auth/src/server/auth_service/check_existing_user.rs @@ -11,6 +11,8 @@ use super::verify_password_hash; pub enum CheckUserExistsError { #[error("Provided credintels does not belong to any registered user")] NonExistingUser, + #[error("Provided password is wrong")] + WrongPassword, #[error("Something went wrong in the DB: {0}")] DatabaseError(#[from] sqlx::Error), #[error(transparent)] @@ -34,6 +36,8 @@ pub async fn get_stored_password_hash( })? .map(|row| (row.user_id, Secret::new(row.password_hash))); + // tracing::info!("getting stored passwordh hash query: {:?}", query); + Ok(query) } @@ -58,15 +62,20 @@ pub async fn check_user_exists( expected_password_hash = stored_password_hash; } - let _ = spawn_blocking(move || { + if user_id.is_none() { + return Err(CheckUserExistsError::NonExistingUser); + } + + let result_verifying_password = spawn_blocking(move || { verify_password_hash(expected_password_hash, login_request.password) }) .await - .map_err(CheckUserExistsError::UnexpectedError); + .map_err(CheckUserExistsError::UnexpectedError)?; - match user_id { - // the user was successfully added - Some(user_id) => Ok(user_id), - None => Err(CheckUserExistsError::NonExistingUser), + match result_verifying_password { + Ok(_) => {} + Err(_) => return Err(CheckUserExistsError::WrongPassword), } + + Ok(user_id.unwrap()) } diff --git a/auth/src/server/auth_service/mod.rs b/auth/src/server/auth_service/mod.rs index b53a9da..a405c4d 100644 --- a/auth/src/server/auth_service/mod.rs +++ b/auth/src/server/auth_service/mod.rs @@ -74,7 +74,7 @@ impl Auth for AuthenticationService { }, }; - let auth_token = match get_token_redis(self.redis_con.clone(), &user_id).await { + let mut auth_token = match get_token_redis(self.redis_con.clone(), &user_id).await { Ok(e) => e, Err(_) => match get_token_db(&self.db_pool, &user_id).await { Ok(e) => match e { @@ -85,6 +85,50 @@ impl Auth for AuthenticationService { }, }; + let secret_key = self.secrets.jwt_secret.clone(); + auth_token = match auth_token_expired(secret_key.clone(), auth_token.as_str()) { + Ok(token_expired) => { + if token_expired { + tracing::info!("auth token has expired"); + let new_auth_token = match spawn_blocking(move || { + generate_auth_token(secret_key, user_id.to_string().as_str()) + }) + .await + { + Ok(res) => match res { + Ok(e) => e, + Err(_) => { + return Err(Status::internal("Failed to generate auth token")) + } + }, + Err(_) => return Err(Status::internal("Could not create a auth token")), + }; + + if update_token_db(&self.db_pool, &user_id, &new_auth_token) + .await + .is_err() + { + return Err(Status::internal( + "auth token was invalid and could not update auth token in DB", + )); + } + + if store_token_redis(self.redis_con.clone(), &user_id, &new_auth_token) + .await + .is_err() + { + return Err(Status::internal("Could not store auth token into redis")); + } + + new_auth_token + } else { + // returning original auth_token + auth_token + } + } + Err(_) => return Err(Status::internal("Couldn't check expire time of auth token")), + }; + let token = Token { access_token: auth_token, }; @@ -119,7 +163,7 @@ impl Auth for AuthenticationService { } Ok(s) => s, }; - let register_request_arc = Arc::new(reqister_request); + // let register_request_arc = Arc::new(reqister_request); let mut transaction = match self.db_pool.begin().await { Ok(transaction) => transaction, @@ -130,19 +174,14 @@ impl Auth for AuthenticationService { } }; - let user_id = - match register_user_into_db(&mut transaction, register_request_arc.clone()).await { - Err(_) => return Err(Status::internal("Could not retrieve user_id")), - Ok(user_id) => user_id, - }; + let user_id = match register_user_into_db(&mut transaction, reqister_request).await { + Err(_) => return Err(Status::internal("Could not retrieve user_id")), + Ok(user_id) => user_id, + }; let secret_key = self.secrets.jwt_secret.clone(); let auth_token = match spawn_blocking(move || { - generate_auth_token( - secret_key, - user_id.to_string().as_str(), - register_request_arc.clone(), - ) + generate_auth_token(secret_key, user_id.to_string().as_str()) }) .await { @@ -164,7 +203,8 @@ impl Auth for AuthenticationService { Ok(_) => match store_token_redis(self.redis_con.clone(), &user_id, &auth_token).await { Ok(_) => (), Err(e) => { - tracing::error!("Failed to save auth token to redis {:?}", e) + tracing::error!("Failed to save auth token to redis {:?}", e); + return Err(Status::internal("Could not store auth token into redis")); } }, Err(_) => { diff --git a/auth/src/server/auth_service/register.rs b/auth/src/server/auth_service/register.rs index 08f9692..83c7188 100644 --- a/auth/src/server/auth_service/register.rs +++ b/auth/src/server/auth_service/register.rs @@ -3,29 +3,20 @@ use anyhow::Context; use secrecy::ExposeSecret; use sqlx::{Executor, Postgres, Row, Transaction}; -use std::sync::Arc; use tokio::task::spawn_blocking; -// #[derive(thiserror::Error)] -// enum RegisterError { -// Validation, -// } - #[tracing::instrument( name = "Saving new user details into the database", - skip(transaction, register_request_arc) + skip(transaction, register_request) )] pub async fn register_user_into_db( transaction: &mut Transaction<'_, Postgres>, - register_request_arc: Arc, + register_request: RegisterData, ) -> Result { - let register_request = register_request_arc.clone(); - - let password_hash = spawn_blocking(move || { - compute_password_hash(register_request_arc.clone().password.as_ref()) - }) - .await? - .context("failed to hash password")?; + let password_hash = + spawn_blocking(move || compute_password_hash(register_request.password.as_ref())) + .await? + .context("failed to hash password")?; // let password_hash = // compute_password_hash(password_arc.clone().as_ref()).context("failed to hash password")?; diff --git a/chat/src/chat.rs b/chat/src/chat.rs index d63ea2e..2c27da1 100644 --- a/chat/src/chat.rs +++ b/chat/src/chat.rs @@ -11,8 +11,8 @@ pub struct ChatMessage { /// Generated client implementations. pub mod chatting_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] - use tonic::codegen::*; use tonic::codegen::http::Uri; + use tonic::codegen::*; #[derive(Debug, Clone)] pub struct ChattingClient { inner: tonic::client::Grpc, @@ -56,9 +56,8 @@ pub mod chatting_client { >::ResponseBody, >, >, - , - >>::Error: Into + Send + Sync, + >>::Error: + Into + Send + Sync, { ChattingClient::new(InterceptedService::new(inner, interceptor)) } @@ -100,19 +99,17 @@ pub mod chatting_client { tonic::Response>, tonic::Status, > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; + self.inner.ready().await.map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; let codec = tonic::codec::ProstCodec::default(); let path = http::uri::PathAndQuery::from_static("/chat.Chatting/chat"); let mut req = request.into_streaming_request(); - req.extensions_mut().insert(GrpcMethod::new("chat.Chatting", "chat")); + req.extensions_mut() + .insert(GrpcMethod::new("chat.Chatting", "chat")); self.inner.streaming(req, path, codec).await } } @@ -127,8 +124,7 @@ pub mod chatting_server { /// Server streaming response type for the chat method. type chatStream: tonic::codegen::tokio_stream::Stream< Item = std::result::Result, - > - + Send + > + Send + 'static; async fn chat( &self, @@ -158,10 +154,7 @@ pub mod chatting_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService + pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService where F: tonic::service::Interceptor, { @@ -217,22 +210,17 @@ pub mod chatting_server { "/chat.Chatting/chat" => { #[allow(non_camel_case_types)] struct chatSvc(pub Arc); - impl tonic::server::StreamingService - for chatSvc { + impl tonic::server::StreamingService for chatSvc { type Response = super::ChatMessage; type ResponseStream = T::chatStream; - type Future = BoxFuture< - tonic::Response, - tonic::Status, - >; + type Future = + BoxFuture, tonic::Status>; fn call( &mut self, request: tonic::Request>, ) -> Self::Future { let inner = Arc::clone(&self.0); - let fut = async move { - ::chat(&inner, request).await - }; + let fut = async move { ::chat(&inner, request).await }; Box::pin(fut) } } @@ -259,18 +247,14 @@ pub mod chatting_server { }; Box::pin(fut) } - _ => { - Box::pin(async move { - Ok( - http::Response::builder() - .status(200) - .header("grpc-status", "12") - .header("content-type", "application/grpc") - .body(empty_body()) - .unwrap(), - ) - }) - } + _ => Box::pin(async move { + Ok(http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap()) + }), } } } diff --git a/chat/src/lib.rs b/chat/src/lib.rs index 2546f21..932e0c1 100644 --- a/chat/src/lib.rs +++ b/chat/src/lib.rs @@ -1,3 +1,4 @@ +pub mod chat; pub mod configuration; pub mod logging; pub mod proto; diff --git a/chat/src/server/auth_interceptor.rs b/chat/src/server/auth_interceptor.rs index 2933ca8..7c33ef9 100644 --- a/chat/src/server/auth_interceptor.rs +++ b/chat/src/server/auth_interceptor.rs @@ -33,12 +33,8 @@ macro_rules! check_claim_expired { } pub fn auth_interceptor(req: Request<()>) -> Result, Status> { - println!("auth interceptor executed"); - match req.metadata().get("authorization") { Some(t) => { - println!("received value: {:?}", t); - tracing::info!("Reading secrets"); let secrets = match get_secrets() { @@ -65,15 +61,14 @@ pub fn auth_interceptor(req: Request<()>) -> Result, Status> { None => return Err(Status::internal("Couldn't destructure the auth token")), }; - println!("token str: {}", token_str); - let claims: BTreeMap = token_str.verify_with_key(&key).unwrap(); - println!("{:?}", claims); check_claim_key!(&claims, "iss", "Chat-gRPC", "JWT iss does not match"); check_claim_key!(&claims, "sub", "auth token", "JWT sub doese not match"); check_claim_expired!(claims["exp"], "Auth token has expired"); + tracing::info!("access token was valid"); + Ok(req) } None => Err(Status::unauthenticated("no valid auth token")), diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..5e68f90 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "client" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +tokio = { version = "1.0", features = ["full"] } +ratatui = "0.28.0" +crossterm = { version = "0.28.1", features = ["event-stream"] } +futures-timer = "3.0.3" +futures = "0.3.30" +tui-big-text = "0.5.2" +tui-prompts = "0.3.19" +unicode-segmentation = "1.10.1" +tui-popup = "0.4.4" +validator = "0.16.1" +tonic = "0.10" +async-stream = "0.3.5" +random_color = "0.8.0" +tokio-stream = { version = "0.1.15", features = ["sync"] } +auth = { path = "../auth" } +chat = { path = "../chat" } + +[[bin]] +name = "chat-client" +path = "src/bin/client.rs" + +[lib] +path = "src/lib.rs" diff --git a/client/src/api/auth.rs b/client/src/api/auth.rs new file mode 100644 index 0000000..5061c8d --- /dev/null +++ b/client/src/api/auth.rs @@ -0,0 +1,46 @@ +use auth::authentication::{auth_client::AuthClient, LoginRequest, RegisterRequest, Token}; +use tonic::{transport::Channel, Request}; + +const ADDRESS: &str = "http://[::1]:8000"; + +pub struct AuthApi { + client: AuthClient, +} + +impl AuthApi { + pub async fn new() -> AuthApi { + let client = AuthClient::connect(ADDRESS) + .await + .expect("failed to create client"); + + Self { client } + } + + pub async fn login(&mut self, login_request: LoginRequest) -> Result { + let request = Request::new(login_request); + + let login_result = self.client.login(request).await; + + match login_result { + Ok(res) => { + // println!("token: {}", res.into_inner().access_token); + Ok(res.into_inner()) + } + Err(e) => Err(e.message().into()), + } + } + + pub async fn register(&mut self, register_request: RegisterRequest) -> Result { + let request = Request::new(register_request); + + let register_result = self.client.register(request).await; + + match register_result { + Ok(res) => { + // println!("token: {}", res.into_inner().access_token); + Ok(res.into_inner()) + } + Err(e) => Err(e.message().into()), + } + } +} diff --git a/client/src/api/chat.rs b/client/src/api/chat.rs new file mode 100644 index 0000000..3b69f5a --- /dev/null +++ b/client/src/api/chat.rs @@ -0,0 +1,74 @@ +use chat::chat::{chatting_client::ChattingClient, ChatMessage}; +use tokio::sync::mpsc::UnboundedSender; +use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; +use tonic::{ + metadata::MetadataValue, + service::{interceptor::InterceptedService, Interceptor}, + transport::Channel, + Request, Status, +}; + +use crate::events::{Event, Sender}; + +const ADDRESS: &str = "http://[::1]:8001"; + +struct MyInterceptor { + access_token: String, +} + +impl Interceptor for MyInterceptor { + fn call( + &mut self, + mut request: tonic::Request<()>, + ) -> std::result::Result, Status> { + let token: MetadataValue<_> = format!("Bearer {}", self.access_token) + .parse() + .expect("Failed to create access token"); + request.metadata_mut().insert("authorization", token); + Ok(request) + } +} + +pub struct ChatApi { + pub sender: UnboundedSender, +} + +impl ChatApi { + pub async fn new(access_token: String, event_sender: Sender) -> Self { + let channel = Channel::from_static(ADDRESS) + .connect() + .await + .expect("Failed to connect to chat service"); + + let mut client: ChattingClient> = + ChattingClient::with_interceptor(channel, MyInterceptor { access_token }); + + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let mut messages_to_send = UnboundedReceiverStream::new(rx); + + tokio::spawn(async move { + let outbound = async_stream::stream! { + while let Some(message) = messages_to_send.next().await { + yield message + } + }; + + let response = client + .chat(Request::new(outbound)) + .await + .expect("Failed to get message"); + + let mut inbound = response.into_inner(); + + while let Some(message) = inbound.message().await.expect("Failed to read") { + let _ = event_sender.send(Event::Message(message)); + } + }); + + Self { sender: tx } + } + + pub async fn chat(&mut self, chat_message: ChatMessage) { + let _ = self.sender.send(chat_message); + } +} diff --git a/client/src/api/mod.rs b/client/src/api/mod.rs new file mode 100644 index 0000000..f7581f3 --- /dev/null +++ b/client/src/api/mod.rs @@ -0,0 +1,5 @@ +mod auth; +mod chat; + +pub use auth::*; +pub use chat::*; diff --git a/client/src/app.rs b/client/src/app.rs new file mode 100644 index 0000000..e7259b7 --- /dev/null +++ b/client/src/app.rs @@ -0,0 +1,55 @@ +use crate::components::{Footer, Home}; + +#[derive(PartialEq)] +pub enum AppMode { + View, + Write, + Error, +} + +#[derive(PartialEq)] +pub enum AppView { + Home, + Login, + Register, + Chat, +} + +pub struct App<'a> { + pub should_quit: bool, + pub view: AppView, + pub mode: AppMode, + pub home: Home<'a>, + pub footer: Footer, + pub username: String, +} + +impl<'a> App<'a> { + pub async fn new() -> App<'a> { + Self { + should_quit: false, + view: AppView::Home, + mode: AppMode::View, + home: Home::new(), + footer: Footer::new(), + username: String::new(), + } + } + + pub fn exit(&mut self) { + self.should_quit = true + } + + pub fn set_error_mode(&mut self) { + self.mode = AppMode::Error + } + + pub fn toggle_mode(&mut self) { + match self.mode { + AppMode::View => self.mode = AppMode::Write, + AppMode::Write => self.mode = AppMode::View, + // don't handle error mode + _ => {} + } + } +} diff --git a/client/src/bin/client.rs b/client/src/bin/client.rs new file mode 100644 index 0000000..e22167b --- /dev/null +++ b/client/src/bin/client.rs @@ -0,0 +1,102 @@ +use anyhow::Result; +use chat::chat::ChatMessage; +use client::{ + api::{AuthApi, ChatApi}, + app::App, + events::*, + tui::Tui, +}; +use random_color::RandomColor; +use ratatui::style::Color; + +#[tokio::main] +async fn main() -> Result<()> { + let mut terminal = Tui::default(); + terminal.initalize()?; + + let mut events = EventHandler::new(16); + + let mut app = App::new().await; + + let mut authapi = AuthApi::new().await; + let mut chatapi: Option = None; + + // main program loop + while !app.should_quit { + terminal.draw(&mut app)?; + + match events.next().await? { + Event::Key(key_event) => { + action(&mut app, key_event, events.sender.clone()).await; + } + Event::Tick => {} + Event::Mouse(_) => {} + Event::Error => app.set_error_mode(), + Event::Login => { + let login_request = app.home.login.get_login_request(); + match authapi.login(login_request.clone()).await { + Ok(token) => { + // println!("access token: {}", token.access_token) + chatapi = + Some(ChatApi::new(token.access_token, events.sender.clone()).await); + app.username = login_request.username; + app.home.set_action_to_chat(); + } + Err(error_msg) => { + app.home.login.show_error_popup = true; + app.home.login.error_description = error_msg; + app.set_error_mode(); + } + }; + } + Event::Register => { + let register_request = app.home.register.get_register_request(); + match authapi.register(register_request.clone()).await { + Ok(token) => { + // println!("access token: {}", token.access_token) + chatapi = + Some(ChatApi::new(token.access_token, events.sender.clone()).await); + app.username = register_request.username; + app.home.set_action_to_chat(); + } + Err(error_msg) => { + app.home.register.show_error_popup = true; + app.home.register.error_description = error_msg; + app.set_error_mode(); + } + } + } + Event::Chat => { + let message = app.home.chat.get_message(); + let chat_message = ChatMessage { + username: app.username.clone(), + message: message.into(), + timestamp: None, + }; + + chatapi.as_mut().unwrap().chat(chat_message).await; + } + Event::Message(message) => { + if !app + .home + .chat + .username_to_color + .contains_key(&message.username) + { + let color_rgb = RandomColor::new().to_rgb_array(); + + app.home.chat.username_to_color.insert( + message.username.clone(), + Color::Rgb(color_rgb[0], color_rgb[1], color_rgb[2]), + ); + } + + app.home.chat.chat_messages.push(message); + app.home.chat.reset_message_prompt_state(); + } + } + } + + terminal.exit()?; + Ok(()) +} diff --git a/client/src/components/footer.rs b/client/src/components/footer.rs new file mode 100644 index 0000000..4ff0ac0 --- /dev/null +++ b/client/src/components/footer.rs @@ -0,0 +1,62 @@ +use ratatui::{ + layout::Rect, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::Paragraph, + Frame, +}; + +use crate::app::{App, AppMode}; + +pub struct Footer {} + +impl Default for Footer { + fn default() -> Footer { + Self {} + } +} + +impl Footer { + pub fn new() -> Footer { + Self::default() + } + + pub fn render(&self, frame: &mut Frame, footer_area: Rect, app: &App) { + let footer_text = match app.mode { + AppMode::View => { + vec![ + Span::styled(" VIEW ", Style::default().bg(Color::Blue).bold()), + Span::styled(" Q or Ctrl + c: Quit.", Style::default()), + // Span::styled(" W: Write Mode.", Style::default()), + Span::styled(" Use ↓↑ to move", Style::default()), + ] + } + AppMode::Write => { + let mut text = vec![ + Span::styled(" WRITE ", Style::default().bg(Color::Green).bold()), + // Span::styled(" Esc: go back to view mode. ", Style::default()), + Span::styled(" Esc : Go Back.", Style::default()), + Span::styled(" Enter : Submit.", Style::default()), + // Span::styled(" Ctrl: Quit.", Style::default()), + ]; + + if app.home.chat.chat_shown() { + text.push(Span::styled(" Use ↓↑ to Scroll. ", Style::default())) + } + + text + } + AppMode::Error => { + vec![ + Span::styled(" ERROR ", Style::default().bg(Color::Red).bold()), + Span::styled(" Enter: to dismiss error.", Style::default()), + Span::styled(" Q or Ctrl + c: Quit.", Style::default()), + ] + } + }; + + let footer = Line::from(footer_text); + + frame.render_widget(Paragraph::new(footer).centered(), footer_area) + } +} diff --git a/client/src/components/home/chat/mod.rs b/client/src/components/home/chat/mod.rs new file mode 100644 index 0000000..b0e3ce3 --- /dev/null +++ b/client/src/components/home/chat/mod.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; + +use chat::chat::ChatMessage; +use crossterm::event::KeyEvent; +use ratatui::{ + layout::{Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{ + Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap, + }, + Frame, +}; +use tui_popup::Popup; +use tui_prompts::{FocusState, Prompt, State, TextPrompt, TextState}; + +use crate::events::{Event, Sender}; + +pub struct Chat<'a> { + show_chat: bool, + message_prompt_state: TextState<'a>, + pub vertical_scroll: u16, + pub username_to_color: HashMap, + pub chat_messages: Vec, + pub show_error_popup: bool, + pub error_description: String, +} + +impl<'a> Default for Chat<'a> { + fn default() -> Chat<'a> { + Self { + show_chat: false, + message_prompt_state: TextState::default().with_focus(FocusState::Focused), + vertical_scroll: 0, + username_to_color: HashMap::new(), + chat_messages: Vec::new(), + show_error_popup: false, + error_description: String::from(""), + } + } +} + +impl<'a> Chat<'a> { + pub fn new() -> Chat<'a> { + Self::default() + } + + pub fn chat_shown(&self) -> bool { + self.show_chat + } + + pub fn toggle_chat(&mut self) { + self.show_chat = !self.show_chat + } + + pub fn handle_event(&mut self, key_event: KeyEvent) { + self.message_prompt_state.handle_key_event(key_event) + } + + pub fn get_message(&self) -> &str { + self.message_prompt_state.value() + } + + pub fn reset_message_prompt_state(&mut self) { + self.message_prompt_state = TextState::default().with_focus(FocusState::Focused) + } + + pub fn handle_submit(&mut self, sender: Sender) { + let message = self.message_prompt_state.value(); + // println!("message to send: {}", message); + + match message.is_empty() { + true => { + self.show_error_popup = true; + self.error_description = "Cannot Send Empty Message".into(); + sender.send(Event::Error).unwrap(); + } + false => { + self.show_error_popup = false; + let _ = sender.send(Event::Chat); + } + } + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(2), + Constraint::Min(1), + Constraint::Percentage(15), + ]) + .split(area); + + //clearing our the area where the pop will be on top + frame.render_widget(Clear, area); + + TextPrompt::from("Message Prompt") + .with_block(Block::bordered()) + .draw(frame, layout[2], &mut self.message_prompt_state); + + let items: Vec = self + .chat_messages + .iter() + .map(|chat_message| { + let color = self.username_to_color.get(&chat_message.username).unwrap(); + Line::from(vec![ + Span::styled( + format!(" User: {}", chat_message.username), + Style::default().fg(*color), + ), + Span::styled(" - ", Style::default().fg(*color)), + Span::styled( + format!("{}", chat_message.message), + Style::default().fg(*color), + ), + ]) + }) + .collect(); + + frame.render_widget( + Paragraph::new(items.clone()) + .wrap(Wrap { trim: false }) + .block(Block::default().title("Messages").borders(Borders::ALL)) + .scroll((self.vertical_scroll, 0)), + layout[1], + ); + + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + let mut scrollbar_state = + ScrollbarState::new(items.len()).position(self.vertical_scroll as usize); + + frame.render_stateful_widget( + scrollbar, + layout[1].inner(Margin { + // using an inner vertical margin of 1 unit makes the scrollbar inside the block + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + + if self.show_error_popup { + let error_popup = Popup::new(self.error_description.as_str()) + .title("Chat Error") + .style(Style::default().on_red()); + + frame.render_widget(&error_popup, area) + } + } +} diff --git a/client/src/components/home/login/mod.rs b/client/src/components/home/login/mod.rs new file mode 100644 index 0000000..bb585d5 --- /dev/null +++ b/client/src/components/home/login/mod.rs @@ -0,0 +1,235 @@ +use auth::authentication::LoginRequest; +use tui_popup::Popup; + +use crossterm::event::KeyEvent; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Clear, Padding, Paragraph}, + Frame, +}; +use tui_prompts::{Prompt, State, TextPrompt, TextRenderStyle, TextState}; + +use crate::{ + events::{Event, Sender}, + ui::centered_rect, +}; + +use super::{validate_password, validate_username}; + +#[derive(Default)] +enum Field { + #[default] + Username, + Password, +} + +pub struct Login<'a> { + show_login: bool, + current_field: Field, + username_state: TextState<'a>, + password_state: TextState<'a>, + pub show_error_popup: bool, + pub error_description: String, +} + +impl<'a> Default for Login<'a> { + fn default() -> Login<'a> { + Self { + show_login: false, + current_field: Field::default(), + username_state: TextState::default(), + password_state: TextState::default(), + show_error_popup: false, + error_description: String::from(""), + } + } +} + +impl<'a> Login<'a> { + pub fn new() -> Login<'a> { + Self::default() + } + + pub fn toggle_login(&mut self) { + self.show_login = !self.show_login + } + + pub fn show_login_error_popup(&self) -> bool { + self.show_error_popup + } + + pub fn is_finished(&self) -> bool { + self.username_state.is_finished() && self.password_state.is_finished() + } + + pub fn get_login_request(&self) -> LoginRequest { + let username = self.username_state.value(); + let password = self.password_state.value(); + LoginRequest { + username: username.into(), + password: password.into(), + } + } + + pub fn reset_textfields_state(&mut self) { + self.username_state = TextState::default(); + self.password_state = TextState::default(); + } + + pub fn focus_next(&mut self) { + self.current_state().blur(); + if let Some(field) = self.next_field() { + self.current_field = field; + } + self.current_state().focus(); + } + + pub fn focus_prev(&mut self) { + self.current_state().blur(); + if let Some(field) = self.prev_field() { + self.current_field = field + } + self.current_state().focus(); + } + + fn focus_current_field(&mut self) { + self.current_state().focus(); + } + + pub fn submit(&mut self, sender: Sender) { + // have to validate the value here then mark it as complete + + let validation_result = match self.current_field { + Field::Username => validate_username(self.current_state().value()), + Field::Password => validate_password(self.current_state().value()), + }; + + match validation_result { + Ok(_) => { + self.show_error_popup = false; + self.current_state().complete(); + + if self.current_state().is_finished() && !self.is_finished() { + self.focus_next() + } else { + // everything is complete + // println!("all done"); + // println!( + // "username: {}, password: {}", + // self.username_state.value(), + // self.password_state.value() + // ) + } + } + Err(e) => { + self.show_error_popup = true; + self.error_description = e; + self.current_state().abort(); + self.current_state().blur(); + sender.send(Event::Error).unwrap(); + } + } + } + + pub fn handle_event_current_field(&mut self, key_event: KeyEvent) { + let state = self.current_state(); + state.handle_key_event(key_event); + } + + fn next_field(&mut self) -> Option { + if !self.current_state().status().is_aborted() { + return match self.current_field { + Field::Username => Some(Field::Password), + Field::Password => Some(Field::Username), + }; + } + + None + } + + fn prev_field(&mut self) -> Option { + if !self.current_state().status().is_aborted() { + return match self.current_field { + Field::Username => Some(Field::Password), + Field::Password => Some(Field::Username), + }; + } + + None + } + + fn current_state(&mut self) -> &mut TextState<'a> { + match self.current_field { + Field::Username => &mut self.username_state, + Field::Password => &mut self.password_state, + } + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let login_block = Block::bordered() + .border_type(BorderType::Rounded) + .padding(Padding::horizontal(2)) + .title("Login Form".bold().into_centered_line()); + + let block_area = centered_rect(45, 25, area); + + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(15), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(2), + ]) + .split(login_block.inner(block_area)); + + //clearing our the area where the pop will be on top + frame.render_widget(Clear, block_area); + frame.render_widget(login_block, block_area); + + TextPrompt::from("Username").draw(frame, layout[1], &mut self.username_state); + + let username_helper_text = vec![ + Line::from(Span::styled("Maximum of 255 character", Style::default())), + Line::from(Span::styled( + "Following charcters are forbidden", + Style::default(), + )), + Line::from(Span::styled( + format!( + "{:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", + '/', '(', ')', '"', '<', '>', '\\', '{', '}' + ), + Style::default().red(), + )), + ]; + + let username_helper_paragraph = Paragraph::new(username_helper_text); + frame.render_widget(username_helper_paragraph, layout[2]); + + TextPrompt::from("Password") + .with_render_style(TextRenderStyle::Password) + .draw(frame, layout[3], &mut self.password_state); + + let password_helper_text = vec![ + Line::from(Span::styled("Minimum of 8 character", Style::default())), + Line::from(Span::styled("Maximum of 255 character", Style::default())), + ]; + let password_helper_paragraph = Paragraph::new(password_helper_text); + frame.render_widget(password_helper_paragraph, layout[4]); + + if self.show_error_popup { + let error_popup = Popup::new(self.error_description.as_str()) + .title("Login Error") + .style(Style::default().on_red()); + + frame.render_widget(&error_popup, area) + } else { + // when we have an error we enter error mode which will unfoucus the current field thus we have to focus back the current field + self.focus_current_field() + } + } +} diff --git a/client/src/components/home/mod.rs b/client/src/components/home/mod.rs new file mode 100644 index 0000000..ff53e11 --- /dev/null +++ b/client/src/components/home/mod.rs @@ -0,0 +1,133 @@ +mod chat; +mod login; +mod register; +mod validation; + +pub use chat::*; +pub use login::*; +pub use register::*; +pub use validation::*; + +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::Stylize; +use ratatui::text::Text; +use ratatui::widgets::ListState; +use ratatui::Frame; +use ratatui::{ + style::{Color, Style}, + widgets::{List, ListDirection}, +}; +use tui_big_text::{BigText, PixelSize}; + +const TITLE: &str = "Chat gRPC"; + +pub enum Action { + Login, + Register, + Chat, +} + +pub struct Home<'a> { + list_items: Vec, + list_state: ListState, + selected_action: Option, + pub login: Login<'a>, + pub register: Register<'a>, + pub chat: Chat<'a>, +} + +impl<'a> Default for Home<'a> { + fn default() -> Home<'a> { + Self { + list_items: vec!["Login".into(), "Register".into()], + list_state: ListState::default().with_selected(Some(0)), + selected_action: None, + login: Login::new(), + register: Register::new(), + chat: Chat::new(), + } + } +} + +impl<'a> Home<'a> { + pub fn new() -> Home<'a> { + Self::default() + } + + pub fn select_next(&mut self) { + self.list_state.select_next(); + } + + pub fn select_previous(&mut self) { + self.list_state.select_previous(); + } + + pub fn selected_action(&self) -> Option<&Action> { + self.selected_action.as_ref() + } + + pub fn reset_action(&mut self) { + self.selected_action = None; + self.login.reset_textfields_state(); + self.register.reset_textfields_state(); + } + + pub fn set_action_to_chat(&mut self) { + self.selected_action = Some(Action::Chat) + } + + pub fn select(&mut self) { + if let Some(i) = self.list_state.selected() { + // println!("choose : {}", self.list_items[i]); + if i == 0 { + self.selected_action = Some(Action::Login); + } else if i == 1 { + // register actions + self.selected_action = Some(Action::Register); + } + } + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Ratio(1, 3), Constraint::Min(1)]) + .vertical_margin(5) + .split(area); + + let title_paragraph = BigText::builder() + .pixel_size(PixelSize::Full) + .centered() + .lines(vec![TITLE.into()]) + .build(); + + frame.render_widget(title_paragraph, layout[0]); + + // we haven't selected an action so the default home page options is shown + if self.selected_action.is_none() { + let items: Vec = self + .list_items + .iter() + .map(|item| { + Text::from(item.clone()) + .centered() + .style(Style::default().bold()) + }) + .collect(); + + let list = List::new(items) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().reversed()) + .highlight_spacing(ratatui::widgets::HighlightSpacing::Always) + .direction(ListDirection::TopToBottom); + + frame.render_stateful_widget(list, layout[1], &mut self.list_state); + } else { + match self.selected_action.as_ref().unwrap() { + Action::Login => self.login.render(frame, area), + Action::Register => self.register.render(frame, area), + Action::Chat => self.chat.render(frame, area), + } + } + } +} diff --git a/client/src/components/home/register/mod.rs b/client/src/components/home/register/mod.rs new file mode 100644 index 0000000..3e43fab --- /dev/null +++ b/client/src/components/home/register/mod.rs @@ -0,0 +1,278 @@ +use auth::authentication::RegisterRequest; +use crossterm::event::KeyEvent; +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Block, BorderType, Clear, Padding, Paragraph}, + Frame, +}; +use tui_popup::Popup; +use tui_prompts::{Prompt, State, TextPrompt, TextRenderStyle, TextState}; + +use crate::{ + events::{Event, Sender}, + ui::centered_rect, +}; + +use super::{parse_email, validate_name, validate_password, validate_username}; + +enum Field { + Firstname, + Lastname, + Username, + Email, + Password, +} + +pub struct Register<'a> { + show_register: bool, + current_field: Field, + firstname_state: TextState<'a>, + lastname_state: TextState<'a>, + username_state: TextState<'a>, + email_state: TextState<'a>, + password_state: TextState<'a>, + pub show_error_popup: bool, + pub error_description: String, +} + +impl<'a> Default for Register<'a> { + fn default() -> Register<'a> { + Self { + show_register: false, + current_field: Field::Firstname, + firstname_state: TextState::default(), + lastname_state: TextState::default(), + username_state: TextState::default(), + email_state: TextState::default(), + password_state: TextState::default(), + show_error_popup: false, + error_description: "".into(), + } + } +} + +impl<'a> Register<'a> { + pub fn new() -> Register<'a> { + Self::default() + } + + pub fn toggle_register(&mut self) { + self.show_register = !self.show_register + } + + pub fn is_finished(&self) -> bool { + self.firstname_state.is_finished() + && self.lastname_state.is_finished() + && self.username_state.is_finished() + && self.email_state.is_finished() + && self.password_state.is_finished() + } + + pub fn get_register_request(&self) -> RegisterRequest { + RegisterRequest { + firstname: self.firstname_state.value().into(), + lastname: self.lastname_state.value().into(), + username: self.username_state.value().into(), + email: self.email_state.value().into(), + password: self.password_state.value().into(), + } + } + + fn focus_current_field(&mut self) { + self.current_state().focus(); + } + + pub fn reset_textfields_state(&mut self) { + self.firstname_state = TextState::default(); + self.lastname_state = TextState::default(); + self.username_state = TextState::default(); + self.email_state = TextState::default(); + self.password_state = TextState::default(); + } + + pub fn focus_next(&mut self) { + self.current_state().blur(); + if let Some(field) = self.next_field() { + self.current_field = field; + } + self.current_state().focus(); + } + + pub fn focus_prev(&mut self) { + self.current_state().blur(); + if let Some(field) = self.prev_field() { + self.current_field = field; + } + self.current_state().focus(); + } + + pub fn submit(&mut self, sender: Sender) { + let validation_result = match self.current_field { + Field::Firstname => validate_name(self.current_state().value(), "Firstname"), + Field::Lastname => validate_name(self.current_state().value(), "Lastname"), + Field::Username => validate_username(self.current_state().value()), + Field::Email => parse_email(self.current_state().value()), + Field::Password => validate_password(self.current_state().value()), + }; + + match validation_result { + Ok(_) => { + self.show_error_popup = false; + self.current_state().complete(); + + if self.current_state().is_finished() && !self.is_finished() { + self.focus_next(); + } else { + // complete + // println!("alles god") + } + } + Err(e) => { + self.show_error_popup = true; + self.error_description = e; + self.current_state().abort(); + self.current_state().blur(); + let _ = sender.send(Event::Error); + } + } + } + + pub fn handle_event_current_field(&mut self, key_event: KeyEvent) { + let state = self.current_state(); + state.handle_key_event(key_event); + } + + fn next_field(&mut self) -> Option { + if !self.current_state().status().is_aborted() { + return match self.current_field { + Field::Firstname => Some(Field::Lastname), + Field::Lastname => Some(Field::Username), + Field::Username => Some(Field::Email), + Field::Email => Some(Field::Password), + Field::Password => Some(Field::Firstname), + }; + } + + None + } + + fn prev_field(&mut self) -> Option { + if !self.current_state().status().is_aborted() { + return match self.current_field { + Field::Firstname => Some(Field::Password), + Field::Lastname => Some(Field::Firstname), + Field::Username => Some(Field::Lastname), + Field::Email => Some(Field::Username), + Field::Password => Some(Field::Email), + }; + } + + None + } + + fn current_state(&mut self) -> &mut TextState<'a> { + match self.current_field { + Field::Firstname => &mut self.firstname_state, + Field::Lastname => &mut self.lastname_state, + Field::Username => &mut self.username_state, + Field::Email => &mut self.email_state, + Field::Password => &mut self.password_state, + } + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let register_block = Block::bordered() + .border_type(BorderType::Rounded) + .padding(Padding::horizontal(2)) + .title("Register Form".bold().into_centered_line()); + + let block_area = centered_rect(45, 45, area); + + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(10), + // firstname + Constraint::Length(1), + Constraint::Length(3), + // lastname + Constraint::Length(1), + Constraint::Length(3), + // username + Constraint::Length(1), + Constraint::Length(4), + //email + Constraint::Length(2), + // password + Constraint::Length(1), + Constraint::Length(2), + ]) + .split(register_block.inner(block_area)); + + frame.render_widget(Clear, block_area); + frame.render_widget(register_block, block_area); + + TextPrompt::from("Firstname").draw(frame, layout[1], &mut self.firstname_state); + + let name_helper_text = vec![ + Line::from(Span::styled("Maximum of 255 character", Style::default())), + Line::from(Span::styled( + "Cannot contains numbers", + Style::default().red(), + )), + ]; + + let name_helper_text = Paragraph::new(name_helper_text); + frame.render_widget(name_helper_text.clone(), layout[2]); + + TextPrompt::from("Lastname").draw(frame, layout[3], &mut self.lastname_state); + + frame.render_widget(name_helper_text, layout[4]); + + TextPrompt::from("Username").draw(frame, layout[5], &mut self.username_state); + + let username_helper_text = vec![ + Line::from(Span::styled("Maximum of 255 character", Style::default())), + Line::from(Span::styled( + "Following charcters are forbidden", + Style::default(), + )), + Line::from(Span::styled( + format!( + "{:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}", + '/', '(', ')', '"', '<', '>', '\\', '{', '}' + ), + Style::default().red(), + )), + ]; + + let username_helper_paragraph = Paragraph::new(username_helper_text); + frame.render_widget(username_helper_paragraph, layout[6]); + + TextPrompt::from("Email").draw(frame, layout[7], &mut self.email_state); + + TextPrompt::from("Password") + .with_render_style(TextRenderStyle::Password) + .draw(frame, layout[8], &mut self.password_state); + + let password_helper_text = vec![ + Line::from(Span::styled("Minimum of 8 character", Style::default())), + Line::from(Span::styled("Maximum of 255 character", Style::default())), + ]; + let password_helper_paragraph = Paragraph::new(password_helper_text); + frame.render_widget(password_helper_paragraph, layout[9]); + + if self.show_error_popup { + // popup error goes here + let error_popup = Popup::new(self.error_description.as_str()) + .title("Register Error") + .style(Style::default().on_red()); + + frame.render_widget(&error_popup, area) + } else { + self.focus_current_field() + } + } +} diff --git a/client/src/components/home/validation.rs b/client/src/components/home/validation.rs new file mode 100644 index 0000000..83a65bf --- /dev/null +++ b/client/src/components/home/validation.rs @@ -0,0 +1,98 @@ +use unicode_segmentation::UnicodeSegmentation; +use validator::validate_email; + +// username consts +const FORBIDDEN_CHARACTERS: [char; 9] = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; +const MAX_USERNAME_LENGTH: u8 = 255; + +// password consts +const MAX_PASSWORD_LENGTH: u8 = 255; +const MIN_PASSWORD_LENGTH: u8 = 8; + +// names consts +const MAX_NAME_LENGTH: u8 = 255; + +pub fn validate_username(username: &str) -> Result<(), String> { + // is_empty_or_whitespace + if username.trim().is_empty() { + return Err("Username is empty or contains to many whitespaces".into()); + } + + // is_too_long + if username.graphemes(true).count() > MAX_USERNAME_LENGTH.into() { + return Err(format!( + "Username is longer than {} chars", + MAX_USERNAME_LENGTH + )); + } + + let is_forbidden_char = username.chars().find(|c| FORBIDDEN_CHARACTERS.contains(c)); + + // contains_forbidden_characters + if let Some(forbidden_char) = is_forbidden_char { + return Err(format!( + "Username contains '{}' which is a forbidden char", + forbidden_char + )); + } + + Ok(()) +} + +pub fn validate_password(password: &str) -> Result<(), String> { + // is_empty_or_whitespace + if password.trim().is_empty() { + return Err("Password is empty or contains to many whitespaces".into()); + } + + // is_not_min_length + if password.graphemes(true).count() < MIN_PASSWORD_LENGTH.into() { + return Err(format!( + "Password is less than {} chars", + MIN_PASSWORD_LENGTH + )); + } + + // is_too_long + if password.graphemes(true).count() > MAX_PASSWORD_LENGTH.into() { + return Err(format!( + "Password is longer than {} chars", + MAX_PASSWORD_LENGTH + )); + } + + Ok(()) +} + +pub fn parse_email(s: &str) -> Result<(), String> { + if validate_email(s) { + Ok(()) + } else { + Err("Email is not valid".into()) + } +} + +pub fn validate_name(s: &str, quantity: &str) -> Result<(), String> { + // is_empty_or_whitespace + if s.trim().is_empty() { + return Err(format!( + "{} is empty or contains too many whitespaces", + quantity + )); + } + + // is_too_long + if s.graphemes(true).count() > MAX_NAME_LENGTH.into() { + return Err(format!( + "{} is longer than {} chars", + quantity, MAX_NAME_LENGTH + )); + } + + // is_contain_numbers + if s.chars().any(|c| c.is_numeric()) { + return Err(format!("{} contains numbers", quantity)); + } + + Ok(()) +} diff --git a/client/src/components/mod.rs b/client/src/components/mod.rs new file mode 100644 index 0000000..059ee40 --- /dev/null +++ b/client/src/components/mod.rs @@ -0,0 +1,5 @@ +mod footer; +mod home; + +pub use footer::*; +pub use home::*; diff --git a/client/src/events/event.rs b/client/src/events/event.rs new file mode 100644 index 0000000..2f109c5 --- /dev/null +++ b/client/src/events/event.rs @@ -0,0 +1,145 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +use crate::{ + app::{App, AppMode}, + components::Action, +}; + +use super::{Event, Sender}; + +pub async fn action<'a>(app: &mut App<'a>, key_event: KeyEvent, sender: Sender) { + match app.mode { + crate::app::AppMode::View => match key_event.code { + KeyCode::Char('q') => { + app.exit(); + } + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.exit() + } + } + // KeyCode::Char('w') => app.toggle_mode(), + KeyCode::Char('j') | KeyCode::Down => app.home.select_next(), + KeyCode::Char('k') | KeyCode::Up => app.home.select_previous(), + KeyCode::Enter => { + app.home.select(); + app.toggle_mode() + } + // for key events which we don't care about + _ => {} + }, + crate::app::AppMode::Write => match key_event.code { + // KeyCode::Esc => app.toggle_mode(), + KeyCode::Enter => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => { + app.home.login.submit(sender.clone()); + match app.home.login.is_finished() { + true => { + let _ = sender.send(Event::Login); + } + false => {} + } + } + Action::Register => { + app.home.register.submit(sender.clone()); + match app.home.register.is_finished() { + true => { + let _ = sender.send(Event::Register); + } + false => {} + } + } + Action::Chat => { + // println!("sending chat event"); + app.home.chat.handle_submit(sender.clone()); + // let _ = sender.send(Event::Chat); + } + } + } + } + + KeyCode::Tab => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => app.home.login.focus_next(), + Action::Register => app.home.register.focus_next(), + Action::Chat => todo!(), + } + } + } + KeyCode::BackTab => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => app.home.login.focus_prev(), + Action::Register => app.home.register.focus_prev(), + Action::Chat => todo!(), + } + } + } + + KeyCode::Esc => { + app.home.reset_action(); + app.toggle_mode(); + } + KeyCode::Up => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => {} + Action::Register => {} + Action::Chat => { + // println!("scrolling up"); + app.home.chat.vertical_scroll = + app.home.chat.vertical_scroll.saturating_add(1); + } + } + } + } + KeyCode::Down => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => {} + Action::Register => {} + Action::Chat => { + // println!("scrolling down"); + app.home.chat.vertical_scroll = + app.home.chat.vertical_scroll.saturating_sub(1); + } + } + } + } + // we are writing + _ => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => app.home.login.handle_event_current_field(key_event), + Action::Register => app.home.register.handle_event_current_field(key_event), + Action::Chat => app.home.chat.handle_event(key_event), + } + } + } + }, + crate::app::AppMode::Error => match key_event.code { + KeyCode::Enter | KeyCode::Esc => { + if let Some(action) = app.home.selected_action() { + match action { + Action::Login => app.home.login.show_error_popup = false, + Action::Register => app.home.register.show_error_popup = false, + Action::Chat => app.home.chat.show_error_popup = false, + } + } + app.mode = AppMode::Write; + } + KeyCode::Char('q') => { + app.exit(); + } + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.exit() + } + } + _ => {} + }, + } +} diff --git a/client/src/events/event_handler.rs b/client/src/events/event_handler.rs new file mode 100644 index 0000000..3975dfe --- /dev/null +++ b/client/src/events/event_handler.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use chat::chat::ChatMessage; +use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; +use futures::{FutureExt, StreamExt}; +use std::time::Duration; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +#[derive(Debug)] +pub enum Event { + Tick, + Key(KeyEvent), + Mouse(MouseEvent), + Error, + Login, + Register, + Chat, + Message(ChatMessage), +} + +pub type Sender = UnboundedSender; +pub type Receiver = UnboundedReceiver; + +#[derive(Debug)] +pub struct EventHandler { + pub sender: Sender, + receiver: Receiver, + _handle: tokio::task::JoinHandle<()>, +} + +impl EventHandler { + pub fn new(tick_rate: u64) -> Self { + let tick_rate = Duration::from_millis(tick_rate); + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let sender = tx.clone(); + + let handler = tokio::spawn(async move { + let mut reader = crossterm::event::EventStream::new(); + let mut tick = tokio::time::interval(tick_rate); + loop { + // initiate the tick + let tick_delay = tick.tick(); + let crossterm_event = reader.next().fuse(); + tokio::select! { + _ = tick_delay => { + sender.send(Event::Tick).unwrap(); + } + + Some(Ok(evt)) = crossterm_event => { + match evt { + CrosstermEvent::Key(key) => { + if key.kind == event::KeyEventKind::Press { + sender.send(Event::Key(key)).unwrap(); + } + }, + CrosstermEvent::Mouse(mouse) => { + sender.send(Event::Mouse(mouse)).unwrap(); + }, + CrosstermEvent::Resize(_, _) => {}, + CrosstermEvent::FocusLost => {}, + CrosstermEvent::FocusGained => {}, + CrosstermEvent::Paste(_) => {}, + } + } + }; + } + }); + + Self { + sender: tx, + receiver: rx, + _handle: handler, + } + } + + pub async fn next(&mut self) -> Result { + self.receiver.recv().await.ok_or_else(|| { + anyhow::anyhow!("Something went wrong when receiving events from events handler") + }) + } +} diff --git a/client/src/events/mod.rs b/client/src/events/mod.rs new file mode 100644 index 0000000..f957bcd --- /dev/null +++ b/client/src/events/mod.rs @@ -0,0 +1,7 @@ +// https://ratatui.rs/tutorials/counter-async-app/async-event-stream/ + +mod event; +mod event_handler; + +pub use event::*; +pub use event_handler::*; diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 0000000..539960a --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,6 @@ +pub mod api; +pub mod app; +pub mod components; +pub mod events; +pub mod tui; +pub mod ui; diff --git a/client/src/tui.rs b/client/src/tui.rs new file mode 100644 index 0000000..f5fdb84 --- /dev/null +++ b/client/src/tui.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use ratatui::{ + backend::CrosstermBackend, + crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + }, + Terminal, +}; +use std::io::{stdout, Stdout}; + +use crate::{app::App, ui::render}; + +/// A type alias for the terminal type used in this application +pub type CrosstermTerminal = Terminal>; + +pub struct Tui { + terminal: CrosstermTerminal, +} + +impl Default for Tui { + fn default() -> Self { + Self::new() + } +} + +impl Tui { + pub fn new() -> Tui { + let backend = CrosstermBackend::new(stdout()); + let terminal = Terminal::new(backend).expect("Failed to create terminal"); + Self { terminal } + } + + pub fn initalize(&mut self) -> Result<()> { + execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?; + enable_raw_mode()?; + + self.terminal.hide_cursor()?; + self.terminal.clear()?; + + Ok(()) + } + + pub fn draw(&mut self, app: &mut App) -> Result<()> { + self.terminal.draw(|frame| render(frame, app))?; + Ok(()) + } + + pub fn exit(&mut self) -> Result<()> { + execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?; + disable_raw_mode()?; + self.terminal.show_cursor()?; + + Ok(()) + } +} diff --git a/client/src/ui/mod.rs b/client/src/ui/mod.rs new file mode 100644 index 0000000..46dede0 --- /dev/null +++ b/client/src/ui/mod.rs @@ -0,0 +1,27 @@ +mod utils; + +pub use utils::*; + +use ratatui::{ + layout::{Constraint, Direction, Layout}, + Frame, +}; + +use crate::app::{App, AppView}; + +pub fn render(frame: &mut Frame, app: &mut App) { + let full_original_framesize = frame.size(); + + let main_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(3)]) + .split(full_original_framesize); + + if app.view == AppView::Home { + app.home.render(frame, main_layout[0]) + } + + app.footer.render(frame, main_layout[1], app) + + // f.render_widget(Paragraph::new("Hello, World! (press 'q' to quit)"), area) +} diff --git a/client/src/ui/utils.rs b/client/src/ui/utils.rs new file mode 100644 index 0000000..e08b3d8 --- /dev/null +++ b/client/src/ui/utils.rs @@ -0,0 +1,17 @@ +use ratatui::layout::{Constraint, Layout, Rect}; + +pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::vertical([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::horizontal([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +}