Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ced1a36d4 | ||
|
|
45fff34c4c | ||
|
|
c84fa4a692 | ||
|
|
eba3f56ba3 | ||
|
|
71ab588c16 | ||
|
|
195375c083 | ||
|
|
34dafcc23e | ||
|
|
507f86fcf1 | ||
|
|
f40654d2c4 | ||
|
|
cd32c175a4 | ||
|
|
9393294af8 | ||
|
|
24c426229c | ||
|
|
3ed6fd4ee8 | ||
|
|
70d83c284a | ||
|
|
8a248cab58 | ||
|
|
e6851e1fe4 | ||
|
|
65ff1256aa | ||
|
|
36dc4302a0 | ||
|
|
938a1f16f1 | ||
|
|
355aff3032 | ||
|
|
3bb771187a | ||
|
|
aa3ff18f9c | ||
|
|
1fc9a0e1ff | ||
|
|
cdac78c1bc | ||
|
|
bdb6cd4069 | ||
|
|
ec5802b3a2 | ||
|
|
1eb2edc1df | ||
|
|
2da009eede | ||
|
|
b2fd44df49 | ||
|
|
78e8cce08b | ||
|
|
2cf4cd6748 | ||
|
|
7caa4d8c3c | ||
|
|
9917195fc4 | ||
|
|
fbcea1b270 | ||
|
|
87b07db26a | ||
|
|
4481560025 | ||
|
|
d1d33b5752 | ||
|
|
c6c6c5ed81 | ||
|
|
4ddcb34205 | ||
|
|
83393a20e2 | ||
|
|
13d501e6d7 | ||
|
|
993febd204 | ||
|
|
49fe2aa793 | ||
|
|
87a572783a | ||
|
|
ca8dea53fd | ||
|
|
fef2f12c9a | ||
|
|
1a529a70bf | ||
|
|
8da29376ab | ||
|
|
8ad5fedcea | ||
|
|
16a7fa0bcc | ||
|
|
5f6858251c | ||
|
|
73567ae5cf | ||
|
|
fe2d1e4684 | ||
|
|
7d4b043d63 | ||
|
|
04b4220c76 | ||
|
|
841418759b | ||
|
|
ccd76eabdd | ||
|
|
fabe1e0ca7 | ||
|
|
9bf1d065d5 | ||
|
|
62aed812b6 | ||
|
|
a58e976227 | ||
|
|
c198297a5c |
187
Cargo.lock
generated
187
Cargo.lock
generated
@@ -274,6 +274,19 @@ version = "1.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"blowfish",
|
||||||
|
"getrandom 0.3.1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bigdecimal"
|
name = "bigdecimal"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
@@ -323,6 +336,16 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blowfish"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.17.0"
|
version = "3.17.0"
|
||||||
@@ -386,9 +409,19 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "client"
|
name = "client"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"common",
|
"common",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
@@ -424,7 +457,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -782,7 +815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -991,8 +1024,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1452,6 +1487,15 @@ version = "2.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instability"
|
name = "instability"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -1499,6 +1543,21 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "9.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"js-sys",
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lasso"
|
name = "lasso"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -1886,6 +1945,16 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -2030,6 +2099,28 @@ dependencies = [
|
|||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr2"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.94"
|
||||||
@@ -2281,6 +2372,20 @@ dependencies = [
|
|||||||
"tstr",
|
"tstr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.15",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.7"
|
version = "0.9.7"
|
||||||
@@ -2356,7 +2461,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2369,7 +2474,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.9.2",
|
"linux-raw-sys 0.9.2",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2483,12 +2588,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "server"
|
name = "server"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bcrypt",
|
||||||
"chrono",
|
"chrono",
|
||||||
"common",
|
"common",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"jsonwebtoken",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"prost",
|
"prost",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -2504,6 +2611,8 @@ dependencies = [
|
|||||||
"tonic",
|
"tonic",
|
||||||
"tonic-reflection",
|
"tonic-reflection",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2574,6 +2683,18 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_asn1"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sized-chunks"
|
name = "sized-chunks"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
@@ -2678,6 +2799,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2760,6 +2882,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2799,6 +2922,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2825,6 +2949,7 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3029,7 +3154,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.0.1",
|
"rustix 1.0.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3450,6 +3575,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
@@ -3473,6 +3604,46 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validator"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||||
|
dependencies = [
|
||||||
|
"idna",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"validator_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validator_derive"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro-error2",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -3623,7 +3794,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ resolver = "2"
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
# TODO: idk how to do the name, fix later
|
# TODO: idk how to do the name, fix later
|
||||||
# name = "Multieko2"
|
# name = "Multieko2"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
authors = ["Filip Priečinský <filippriec@gmail.com>"]
|
authors = ["Filip Priečinský <filippriec@gmail.com>"]
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
# config.toml
|
# config.toml
|
||||||
[keybindings]
|
[keybindings]
|
||||||
|
|
||||||
|
enter_command_mode = [":", "ctrl+;"]
|
||||||
|
|
||||||
|
[keybindings.general]
|
||||||
|
move_up = ["k", "Up"]
|
||||||
|
move_down = ["j", "Down"]
|
||||||
|
next_option = ["l", "Right"]
|
||||||
|
previous_option = ["h", "Left"]
|
||||||
|
select = ["Enter"]
|
||||||
|
toggle_sidebar = ["ctrl+t"]
|
||||||
|
next_field = ["Tab"]
|
||||||
|
prev_field = ["Shift+Tab"]
|
||||||
|
|
||||||
[keybindings.common]
|
[keybindings.common]
|
||||||
save = ["ctrl+s"]
|
save = ["ctrl+s"]
|
||||||
quit = ["ctrl+q"]
|
quit = ["ctrl+q"]
|
||||||
@@ -33,7 +45,6 @@ move_line_start = ["0"]
|
|||||||
move_line_end = ["$"]
|
move_line_end = ["$"]
|
||||||
move_first_line = ["gg"]
|
move_first_line = ["gg"]
|
||||||
move_last_line = ["x"]
|
move_last_line = ["x"]
|
||||||
enter_command_mode = [":", "ctrl+;"]
|
|
||||||
|
|
||||||
[keybindings.edit]
|
[keybindings.edit]
|
||||||
exit_edit_mode = ["esc","ctrl+e"]
|
exit_edit_mode = ["esc","ctrl+e"]
|
||||||
|
|||||||
4
client/src/components/admin.rs
Normal file
4
client/src/components/admin.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/components/admin.rs
|
||||||
|
pub mod admin_panel;
|
||||||
|
|
||||||
|
pub use admin_panel::*;
|
||||||
118
client/src/components/admin/admin_panel.rs
Normal file
118
client/src/components/admin/admin_panel.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// src/components/admin/admin_panel.rs
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
|
||||||
|
style::Style,
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
|
pub struct AdminPanelState {
|
||||||
|
pub list_state: ListState,
|
||||||
|
pub profiles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdminPanelState {
|
||||||
|
pub fn new(profiles: Vec<String>) -> Self {
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
if !profiles.is_empty() {
|
||||||
|
list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
Self { list_state, profiles }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
let i = self.list_state.selected().map_or(0, |i|
|
||||||
|
if i >= self.profiles.len() - 1 { 0 } else { i + 1 });
|
||||||
|
self.list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
let i = self.list_state.selected().map_or(0, |i|
|
||||||
|
if i == 0 { self.profiles.len() - 1 } else { i - 1 });
|
||||||
|
self.list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
f: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
theme: &Theme,
|
||||||
|
profile_tree: &ProfileTreeResponse,
|
||||||
|
selected_profile: &Option<String>,
|
||||||
|
) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(theme.accent))
|
||||||
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
|
let inner_area = block.inner(area);
|
||||||
|
f.render_widget(block, area);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
||||||
|
.split(inner_area);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = Line::from(Span::styled("Admin Panel", Style::default().fg(theme.highlight)));
|
||||||
|
let title_widget = Paragraph::new(title).alignment(Alignment::Center);
|
||||||
|
f.render_widget(title_widget, chunks[0]);
|
||||||
|
|
||||||
|
// Content
|
||||||
|
let content_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
// Profile list
|
||||||
|
let items: Vec<ListItem> = self.profiles.iter()
|
||||||
|
.map(|p| ListItem::new(Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
if Some(p) == selected_profile.as_ref() { "✓ " } else { " " },
|
||||||
|
Style::default().fg(theme.accent)
|
||||||
|
),
|
||||||
|
Span::styled(p, Style::default().fg(theme.fg)),
|
||||||
|
])))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(Block::default().title("Profiles"))
|
||||||
|
.highlight_style(Style::default().bg(theme.highlight).fg(theme.bg));
|
||||||
|
|
||||||
|
f.render_stateful_widget(list, content_chunks[0], &mut self.list_state);
|
||||||
|
|
||||||
|
// Profile details
|
||||||
|
if let Some(profile) = self.list_state.selected()
|
||||||
|
.and_then(|i| profile_tree.profiles.get(i))
|
||||||
|
{
|
||||||
|
let mut text = Text::default();
|
||||||
|
text.lines.push(Line::from(vec![
|
||||||
|
Span::styled("Profile: ", Style::default().fg(theme.accent)),
|
||||||
|
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
|
||||||
|
]));
|
||||||
|
|
||||||
|
text.lines.push(Line::from(""));
|
||||||
|
text.lines.push(Line::from(Span::styled("Tables:", Style::default().fg(theme.accent))));
|
||||||
|
|
||||||
|
for table in &profile.tables {
|
||||||
|
let mut line = vec![Span::styled(format!("├─ {}", table.name), theme.fg)];
|
||||||
|
if !table.depends_on.is_empty() {
|
||||||
|
line.push(Span::styled(
|
||||||
|
format!(" → {}", table.depends_on.join(", ")),
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
text.lines.push(Line::from(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
let details_widget = Paragraph::new(text)
|
||||||
|
.block(Block::default().title("Details"));
|
||||||
|
f.render_widget(details_widget, content_chunks[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
client/src/components/auth.rs
Normal file
6
client/src/components/auth.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/components/form.rs
|
||||||
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
|
|
||||||
|
pub use login::*;
|
||||||
|
pub use register::*;
|
||||||
111
client/src/components/auth/login.rs
Normal file
111
client/src/components/auth/login.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// src/components/login/login.rs
|
||||||
|
use ratatui::{
|
||||||
|
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
|
||||||
|
style::Style,
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
|
pub struct LoginState {
|
||||||
|
pub fields: Vec<String>,
|
||||||
|
pub values: Vec<String>,
|
||||||
|
pub selected_field: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
fields: vec!["Username".to_string(), "Password".to_string()],
|
||||||
|
values: vec![String::new(), String::new()],
|
||||||
|
selected_field: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_field(&mut self) {
|
||||||
|
self.selected_field = (self.selected_field + 1) % self.fields.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_field(&mut self) {
|
||||||
|
self.selected_field = if self.selected_field == 0 {
|
||||||
|
self.fields.len() - 1
|
||||||
|
} else {
|
||||||
|
self.selected_field - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(theme.accent))
|
||||||
|
.style(Style::default().bg(theme.bg));
|
||||||
|
|
||||||
|
let inner_area = block.inner(area);
|
||||||
|
f.render_widget(block, area);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Min(1),
|
||||||
|
])
|
||||||
|
.split(inner_area);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = Line::from(Span::styled("Login", Style::default().fg(theme.highlight)));
|
||||||
|
let title_widget = Paragraph::new(title).alignment(Alignment::Center);
|
||||||
|
f.render_widget(title_widget, chunks[0]);
|
||||||
|
|
||||||
|
// Login form
|
||||||
|
let form_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
])
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
// Username field
|
||||||
|
let username_block = Block::default()
|
||||||
|
.title("Username")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(if self.selected_field == 0 {
|
||||||
|
Style::default().fg(theme.highlight)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(theme.border)
|
||||||
|
});
|
||||||
|
let username = Paragraph::new(self.values[0].as_str())
|
||||||
|
.block(username_block);
|
||||||
|
f.render_widget(username, form_chunks[0]);
|
||||||
|
|
||||||
|
// Password field
|
||||||
|
let password_block = Block::default()
|
||||||
|
.title("Password")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(if self.selected_field == 1 {
|
||||||
|
Style::default().fg(theme.highlight)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(theme.border)
|
||||||
|
});
|
||||||
|
let password = Paragraph::new("*".repeat(self.values[1].len()))
|
||||||
|
.block(password_block);
|
||||||
|
f.render_widget(password, form_chunks[1]);
|
||||||
|
|
||||||
|
// Submit button
|
||||||
|
let submit_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(if self.selected_field == 2 {
|
||||||
|
Style::default().fg(theme.highlight)
|
||||||
|
} else {
|
||||||
|
Style::default().fg(theme.border)
|
||||||
|
});
|
||||||
|
let submit = Paragraph::new("Submit")
|
||||||
|
.block(submit_block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(submit, form_chunks[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
client/src/components/common.rs
Normal file
8
client/src/components/common.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// src/components/common.rs
|
||||||
|
pub mod command_line;
|
||||||
|
pub mod status_line;
|
||||||
|
pub mod background;
|
||||||
|
|
||||||
|
pub use command_line::*;
|
||||||
|
pub use status_line::*;
|
||||||
|
pub use background::*;
|
||||||
@@ -5,7 +5,7 @@ use ratatui::{
|
|||||||
style::Style,
|
style::Style,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
pub fn render_background(f: &mut Frame, area: Rect, theme: &Theme) {
|
pub fn render_background(f: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
let background = Block::default()
|
let background = Block::default()
|
||||||
@@ -5,7 +5,7 @@ use ratatui::{
|
|||||||
layout::Rect,
|
layout::Rect,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
pub fn render_command_line(f: &mut Frame, area: Rect, input: &str, active: bool, theme: &Theme, message: &str) {
|
pub fn render_command_line(f: &mut Frame, area: Rect, input: &str, active: bool, theme: &Theme, message: &str) {
|
||||||
let prompt = if active {
|
let prompt = if active {
|
||||||
@@ -6,7 +6,7 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub fn render_status_line(
|
pub fn render_status_line(
|
||||||
4
client/src/components/form.rs
Normal file
4
client/src/components/form.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/components/form.rs
|
||||||
|
pub mod form;
|
||||||
|
|
||||||
|
pub use form::*;
|
||||||
@@ -5,9 +5,9 @@ use ratatui::{
|
|||||||
style::Style,
|
style::Style,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::ui::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use super::canvas::render_canvas; // Changed to canvas
|
use crate::components::handlers::canvas::render_canvas;
|
||||||
|
|
||||||
pub fn render_form(
|
pub fn render_form(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
// src/components/handlers.rs
|
// src/components/handlers.rs
|
||||||
pub mod form;
|
|
||||||
pub mod command_line;
|
|
||||||
pub mod status_line;
|
|
||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod background;
|
|
||||||
pub mod intro;
|
|
||||||
|
|
||||||
pub use command_line::render_command_line;
|
|
||||||
pub use form::*;
|
|
||||||
pub use status_line::render_status_line;
|
|
||||||
pub use canvas::*;
|
pub use canvas::*;
|
||||||
pub use sidebar::*;
|
pub use sidebar::*;
|
||||||
pub use background::*;
|
|
||||||
pub use intro::*;
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use ratatui::{
|
|||||||
Frame,
|
Frame,
|
||||||
prelude::Alignment,
|
prelude::Alignment,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::ui::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
|
|
||||||
pub fn render_canvas(
|
pub fn render_canvas(
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ use ratatui::{
|
|||||||
style::Style,
|
style::Style,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use common::proto::multieko2::table_definition::{ProfileTreeResponse};
|
use common::proto::multieko2::table_definition::{ProfileTreeResponse};
|
||||||
use ratatui::text::{Span, Line};
|
use ratatui::text::{Span, Line};
|
||||||
|
|
||||||
const SIDEBAR_WIDTH: u16 = 16;
|
// Reduced sidebar width
|
||||||
|
const SIDEBAR_WIDTH: u16 = 12;
|
||||||
|
|
||||||
pub fn calculate_sidebar_layout(show_sidebar: bool, main_content_area: Rect) -> (Option<Rect>, Rect) {
|
pub fn calculate_sidebar_layout(show_sidebar: bool, main_content_area: Rect) -> (Option<Rect>, Rect) {
|
||||||
if show_sidebar {
|
if show_sidebar {
|
||||||
@@ -26,47 +27,83 @@ pub fn calculate_sidebar_layout(show_sidebar: bool, main_content_area: Rect) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_sidebar(f: &mut Frame, area: Rect, theme: &Theme, profile_tree: &ProfileTreeResponse) {
|
pub fn render_sidebar(
|
||||||
let sidebar_block = Block::default()
|
f: &mut Frame,
|
||||||
.style(Style::default().bg(theme.bg));
|
area: Rect,
|
||||||
|
theme: &Theme,
|
||||||
|
profile_tree: &ProfileTreeResponse,
|
||||||
|
selected_profile: &Option<String>,
|
||||||
|
) {
|
||||||
|
let sidebar_block = Block::default().style(Style::default().bg(theme.bg));
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
|
||||||
for profile in &profile_tree.profiles {
|
if let Some(profile_name) = selected_profile {
|
||||||
// Profile header
|
// Existing code for when a profile is selected...
|
||||||
items.push(ListItem::new(Line::from(vec![
|
} else {
|
||||||
Span::styled("📁 ", Style::default().fg(theme.accent)),
|
// Show full profile tree when no profile is selected (compact version)
|
||||||
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
|
for (profile_idx, profile) in profile_tree.profiles.iter().enumerate() {
|
||||||
])));
|
// Profile header - more compact
|
||||||
|
|
||||||
// Profile tables
|
|
||||||
for (table_idx, table) in profile.tables.iter().enumerate() {
|
|
||||||
let is_last_table = table_idx == profile.tables.len() - 1;
|
|
||||||
let tree_prefix = if is_last_table { "└─ " } else { "├─ " };
|
|
||||||
|
|
||||||
// Table name
|
|
||||||
items.push(ListItem::new(Line::from(vec![
|
items.push(ListItem::new(Line::from(vec![
|
||||||
Span::styled(format!(" {}", tree_prefix), Style::default().fg(theme.fg)),
|
Span::styled("◆", Style::default().fg(theme.accent)),
|
||||||
Span::styled(&table.name, Style::default().fg(theme.fg)),
|
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
|
||||||
])));
|
])));
|
||||||
|
|
||||||
// Dependencies
|
// Tables with compact prefixes
|
||||||
if !table.depends_on.is_empty() {
|
for (table_idx, table) in profile.tables.iter().enumerate() {
|
||||||
let dep_prefix = if is_last_table { " " } else { "│ " };
|
let is_last_table = table_idx == profile.tables.len() - 1;
|
||||||
let deps = table.depends_on.join(", ");
|
let is_last_profile = profile_idx == profile_tree.profiles.len() - 1;
|
||||||
|
|
||||||
items.push(ListItem::new(Line::from(vec![
|
// Shorter prefix characters
|
||||||
Span::styled(format!(" {} └─ ", dep_prefix), Style::default().fg(theme.secondary)),
|
let prefix = match (is_last_profile, is_last_table) {
|
||||||
Span::styled(format!("→ {}", deps), Style::default().fg(theme.secondary)),
|
(true, true) => " └",
|
||||||
])));
|
(true, false) => " ├",
|
||||||
|
(false, true) => "│└",
|
||||||
|
(false, false) => "│├",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get table name without year prefix to save space
|
||||||
|
let display_name = if table.name.starts_with("2025_") {
|
||||||
|
&table.name[5..] // Skip "2025_" prefix
|
||||||
|
} else {
|
||||||
|
&table.name
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut line = vec![
|
||||||
|
Span::styled(prefix, Style::default().fg(theme.fg)),
|
||||||
|
Span::styled(display_name, Style::default().fg(theme.fg)),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Show a simple indicator for dependencies instead of listing them
|
||||||
|
if !table.depends_on.is_empty() {
|
||||||
|
line.push(Span::styled(
|
||||||
|
"→",
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(ListItem::new(Line::from(line)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact separator between profiles
|
||||||
|
if profile_idx < profile_tree.profiles.len() - 1 {
|
||||||
|
items.push(ListItem::new(Line::from(
|
||||||
|
Span::styled("│", Style::default().fg(theme.secondary))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile_tree.profiles.is_empty() {
|
||||||
|
items.push(ListItem::new(Span::styled(
|
||||||
|
"No profiles",
|
||||||
|
Style::default().fg(theme.secondary)
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let list = List::new(items)
|
let list = List::new(items)
|
||||||
.block(sidebar_block)
|
.block(sidebar_block)
|
||||||
.highlight_style(Style::default().fg(theme.highlight))
|
.highlight_style(Style::default().fg(theme.highlight))
|
||||||
.highlight_symbol(">>");
|
.highlight_symbol(">");
|
||||||
|
|
||||||
f.render_widget(list, area);
|
f.render_widget(list, area);
|
||||||
}
|
}
|
||||||
|
|||||||
4
client/src/components/intro.rs
Normal file
4
client/src/components/intro.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/components/intro.rs
|
||||||
|
pub mod intro;
|
||||||
|
|
||||||
|
pub use intro::*;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/components/handlers/intro.rs
|
// src/components/intro/intro.rs
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::Style,
|
style::Style,
|
||||||
@@ -7,7 +7,7 @@ use ratatui::{
|
|||||||
prelude::Margin,
|
prelude::Margin,
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
|
|
||||||
pub struct IntroState {
|
pub struct IntroState {
|
||||||
pub selected_option: usize,
|
pub selected_option: usize,
|
||||||
@@ -33,7 +33,7 @@ impl IntroState {
|
|||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Percentage(35),
|
Constraint::Percentage(35),
|
||||||
Constraint::Length(5),
|
Constraint::Length(7), // Increased to accommodate 3 buttons
|
||||||
Constraint::Percentage(35),
|
Constraint::Percentage(35),
|
||||||
])
|
])
|
||||||
.split(inner_area);
|
.split(inner_area);
|
||||||
@@ -48,10 +48,14 @@ impl IntroState {
|
|||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
f.render_widget(title_para, chunks[1]);
|
f.render_widget(title_para, chunks[1]);
|
||||||
|
|
||||||
// Buttons
|
// Buttons - now with 3 options
|
||||||
let button_area = Layout::default()
|
let button_area = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([
|
||||||
|
Constraint::Percentage(33),
|
||||||
|
Constraint::Percentage(33),
|
||||||
|
Constraint::Percentage(33),
|
||||||
|
])
|
||||||
.split(chunks[1].inner(Margin {
|
.split(chunks[1].inner(Margin {
|
||||||
horizontal: 1,
|
horizontal: 1,
|
||||||
vertical: 1
|
vertical: 1
|
||||||
@@ -71,6 +75,13 @@ impl IntroState {
|
|||||||
self.selected_option == 1,
|
self.selected_option == 1,
|
||||||
theme,
|
theme,
|
||||||
);
|
);
|
||||||
|
self.render_button(
|
||||||
|
f,
|
||||||
|
button_area[2],
|
||||||
|
"Login",
|
||||||
|
self.selected_option == 2,
|
||||||
|
theme,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_button(&self, f: &mut Frame, area: Rect, text: &str, selected: bool, theme: &Theme) {
|
fn render_button(&self, f: &mut Frame, area: Rect, text: &str, selected: bool, theme: &Theme) {
|
||||||
@@ -100,11 +111,23 @@ impl IntroState {
|
|||||||
f.render_widget(button, area);
|
f.render_widget(button, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_option(&mut self) {
|
pub fn next_option(&mut self) {
|
||||||
self.selected_option = (self.selected_option + 1) % 2;
|
self.selected_option = (self.selected_option + 1) % 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_option(&mut self) {
|
pub fn previous_option(&mut self) {
|
||||||
self.selected_option = if self.selected_option == 0 { 1 } else { 0 };
|
self.selected_option = if self.selected_option == 0 { 2 } else { self.selected_option - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_selection(&self, app_state: &mut crate::state::state::AppState) {
|
||||||
|
match self.selected_option {
|
||||||
|
0 => { /* Continue logic */ }
|
||||||
|
1 => { /* Admin logic */ }
|
||||||
|
2 => {
|
||||||
|
app_state.ui.show_intro = false;
|
||||||
|
app_state.ui.show_login = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
// src/components/mod.rs
|
// src/components/mod.rs
|
||||||
pub mod models;
|
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod intro;
|
||||||
|
pub mod admin;
|
||||||
|
pub mod common;
|
||||||
|
pub mod form;
|
||||||
|
pub mod auth;
|
||||||
|
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
|
pub use intro::*;
|
||||||
|
pub use admin::*;
|
||||||
|
pub use common::*;
|
||||||
|
pub use form::*;
|
||||||
|
pub use auth::*;
|
||||||
|
|||||||
7
client/src/config/binds.rs
Normal file
7
client/src/config/binds.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/config/binds.rs
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod key_sequences;
|
||||||
|
|
||||||
|
pub use config::*;
|
||||||
|
pub use key_sequences::*;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// client/src/config/config.rs
|
// src/config/binds/config.rs
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -25,6 +25,8 @@ pub struct Config {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ModeKeybindings {
|
pub struct ModeKeybindings {
|
||||||
|
#[serde(default)]
|
||||||
|
pub general: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub read_only: HashMap<String, Vec<String>>,
|
pub read_only: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -33,7 +35,6 @@ pub struct ModeKeybindings {
|
|||||||
pub command: HashMap<String, Vec<String>>,
|
pub command: HashMap<String, Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub common: HashMap<String, Vec<String>>,
|
pub common: HashMap<String, Vec<String>>,
|
||||||
// Store top-level keybindings that aren't in a specific mode section
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub global: HashMap<String, Vec<String>>,
|
pub global: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,17 @@ impl Config {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_general_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
|
self.get_action_for_key_in_mode(&self.keybindings.general, key, modifiers)
|
||||||
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common actions for Edit/Read-only modes
|
||||||
|
pub fn get_common_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
|
self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets an action for a key in Read-Only mode, also checking common keybindings.
|
/// Gets an action for a key in Read-Only mode, also checking common keybindings.
|
||||||
pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||||
self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)
|
self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)
|
||||||
@@ -70,6 +82,25 @@ impl Config {
|
|||||||
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context-aware keybinding resolution
|
||||||
|
pub fn get_action_for_current_context(
|
||||||
|
&self,
|
||||||
|
is_edit_mode: bool,
|
||||||
|
command_mode: bool,
|
||||||
|
key: KeyCode,
|
||||||
|
modifiers: KeyModifiers
|
||||||
|
) -> Option<&str> {
|
||||||
|
match (command_mode, is_edit_mode) {
|
||||||
|
(true, _) => self.get_command_action_for_key(key, modifiers),
|
||||||
|
(_, true) => self.get_edit_action_for_key(key, modifiers)
|
||||||
|
.or_else(|| self.get_common_action(key, modifiers)),
|
||||||
|
_ => self.get_read_only_action_for_key(key, modifiers)
|
||||||
|
.or_else(|| self.get_common_action(key, modifiers))
|
||||||
|
// Add global bindings check for read-only mode
|
||||||
|
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper function to get an action for a key in a specific mode.
|
/// Helper function to get an action for a key in a specific mode.
|
||||||
pub fn get_action_for_key_in_mode<'a>(
|
pub fn get_action_for_key_in_mode<'a>(
|
||||||
&self,
|
&self,
|
||||||
@@ -355,13 +386,13 @@ impl Config {
|
|||||||
|
|
||||||
// Get string representations of the sequence
|
// Get string representations of the sequence
|
||||||
let sequence_str = sequence.iter()
|
let sequence_str = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
// Add the missing sequence_plus definition
|
// Add the missing sequence_plus definition
|
||||||
let sequence_plus = sequence.iter()
|
let sequence_plus = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("+");
|
.join("+");
|
||||||
|
|
||||||
@@ -414,7 +445,7 @@ impl Config {
|
|||||||
// Special case for + format in bindings
|
// Special case for + format in bindings
|
||||||
if binding.contains('+') {
|
if binding.contains('+') {
|
||||||
let normalized_sequence = sequence.iter()
|
let normalized_sequence = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let binding_parts: Vec<&str> = binding.split('+').collect();
|
let binding_parts: Vec<&str> = binding.split('+').collect();
|
||||||
@@ -442,7 +473,7 @@ impl Config {
|
|||||||
|
|
||||||
// Get string representation of the sequence
|
// Get string representation of the sequence
|
||||||
let sequence_str = sequence.iter()
|
let sequence_str = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
@@ -491,7 +522,7 @@ impl Config {
|
|||||||
if binding.contains('+') {
|
if binding.contains('+') {
|
||||||
let binding_parts: Vec<&str> = binding.split('+').collect();
|
let binding_parts: Vec<&str> = binding.split('+').collect();
|
||||||
let sequence_parts = sequence.iter()
|
let sequence_parts = sequence.iter()
|
||||||
.map(|k| crate::config::key_sequences::key_to_string(k))
|
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
if binding_parts.len() > sequence_parts.len() {
|
if binding_parts.len() > sequence_parts.len() {
|
||||||
@@ -1,68 +1,4 @@
|
|||||||
// src/client/colors.rs
|
// src/config/colors.rs
|
||||||
use ratatui::style::Color;
|
pub mod themes;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
pub use themes::*;
|
||||||
pub struct Theme {
|
|
||||||
pub bg: Color,
|
|
||||||
pub fg: Color,
|
|
||||||
pub accent: Color,
|
|
||||||
pub secondary: Color,
|
|
||||||
pub highlight: Color,
|
|
||||||
pub warning: Color,
|
|
||||||
pub border: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
pub fn from_str(theme_name: &str) -> Self {
|
|
||||||
match theme_name.to_lowercase().as_str() {
|
|
||||||
"dark" => Self::dark(),
|
|
||||||
"high_contrast" => Self::high_contrast(),
|
|
||||||
_ => Self::light(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default light theme
|
|
||||||
pub fn light() -> Self {
|
|
||||||
Self {
|
|
||||||
bg: Color::Rgb(245, 245, 245), // Light gray
|
|
||||||
fg: Color::Rgb(64, 64, 64), // Dark gray
|
|
||||||
accent: Color::Rgb(173, 216, 230), // Pastel blue
|
|
||||||
secondary: Color::Rgb(255, 165, 0), // Orange for secondary
|
|
||||||
highlight: Color::Rgb(152, 251, 152), // Pastel green
|
|
||||||
warning: Color::Rgb(255, 182, 193), // Pastel pink
|
|
||||||
border: Color::Rgb(220, 220, 220), // Light gray border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-contrast dark theme
|
|
||||||
pub fn dark() -> Self {
|
|
||||||
Self {
|
|
||||||
bg: Color::Rgb(30, 30, 30), // Dark background
|
|
||||||
fg: Color::Rgb(255, 255, 255), // White text
|
|
||||||
accent: Color::Rgb(0, 191, 255), // Bright blue
|
|
||||||
secondary: Color::Rgb(255, 215, 0), // Gold for secondary
|
|
||||||
highlight: Color::Rgb(50, 205, 50), // Bright green
|
|
||||||
warning: Color::Rgb(255, 99, 71), // Bright red
|
|
||||||
border: Color::Rgb(100, 100, 100), // Medium gray border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// High-contrast light theme
|
|
||||||
pub fn high_contrast() -> Self {
|
|
||||||
Self {
|
|
||||||
bg: Color::Rgb(255, 255, 255), // White background
|
|
||||||
fg: Color::Rgb(0, 0, 0), // Black text
|
|
||||||
accent: Color::Rgb(0, 0, 255), // Blue
|
|
||||||
secondary: Color::Rgb(255, 140, 0), // Dark orange for secondary
|
|
||||||
highlight: Color::Rgb(0, 128, 0), // Green
|
|
||||||
warning: Color::Rgb(255, 0, 0), // Red
|
|
||||||
border: Color::Rgb(0, 0, 0), // Black border
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Theme {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::light() // Default to light theme
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
68
client/src/config/colors/themes.rs
Normal file
68
client/src/config/colors/themes.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// src/client/themes/colors.rs
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Theme {
|
||||||
|
pub bg: Color,
|
||||||
|
pub fg: Color,
|
||||||
|
pub accent: Color,
|
||||||
|
pub secondary: Color,
|
||||||
|
pub highlight: Color,
|
||||||
|
pub warning: Color,
|
||||||
|
pub border: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
pub fn from_str(theme_name: &str) -> Self {
|
||||||
|
match theme_name.to_lowercase().as_str() {
|
||||||
|
"dark" => Self::dark(),
|
||||||
|
"high_contrast" => Self::high_contrast(),
|
||||||
|
_ => Self::light(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default light theme
|
||||||
|
pub fn light() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: Color::Rgb(245, 245, 245), // Light gray
|
||||||
|
fg: Color::Rgb(64, 64, 64), // Dark gray
|
||||||
|
accent: Color::Rgb(173, 216, 230), // Pastel blue
|
||||||
|
secondary: Color::Rgb(255, 165, 0), // Orange for secondary
|
||||||
|
highlight: Color::Rgb(152, 251, 152), // Pastel green
|
||||||
|
warning: Color::Rgb(255, 182, 193), // Pastel pink
|
||||||
|
border: Color::Rgb(220, 220, 220), // Light gray border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-contrast dark theme
|
||||||
|
pub fn dark() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: Color::Rgb(30, 30, 30), // Dark background
|
||||||
|
fg: Color::Rgb(255, 255, 255), // White text
|
||||||
|
accent: Color::Rgb(0, 191, 255), // Bright blue
|
||||||
|
secondary: Color::Rgb(255, 215, 0), // Gold for secondary
|
||||||
|
highlight: Color::Rgb(50, 205, 50), // Bright green
|
||||||
|
warning: Color::Rgb(255, 99, 71), // Bright red
|
||||||
|
border: Color::Rgb(100, 100, 100), // Medium gray border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-contrast light theme
|
||||||
|
pub fn high_contrast() -> Self {
|
||||||
|
Self {
|
||||||
|
bg: Color::Rgb(255, 255, 255), // White background
|
||||||
|
fg: Color::Rgb(0, 0, 0), // Black text
|
||||||
|
accent: Color::Rgb(0, 0, 255), // Blue
|
||||||
|
secondary: Color::Rgb(255, 140, 0), // Dark orange for secondary
|
||||||
|
highlight: Color::Rgb(0, 128, 0), // Green
|
||||||
|
warning: Color::Rgb(255, 0, 0), // Red
|
||||||
|
border: Color::Rgb(0, 0, 0), // Black border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Theme {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::light() // Default to light theme
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// src/config/mod.rs
|
// src/config/mod.rs
|
||||||
|
|
||||||
|
pub mod binds;
|
||||||
pub mod colors;
|
pub mod colors;
|
||||||
pub mod config;
|
|
||||||
pub mod key_sequences;
|
|
||||||
|
|||||||
4
client/src/modes/canvas.rs
Normal file
4
client/src/modes/canvas.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/client/modes/canvas.rs
|
||||||
|
pub mod edit;
|
||||||
|
pub mod common;
|
||||||
|
pub mod read_only;
|
||||||
@@ -1,9 +1,83 @@
|
|||||||
// src/modes/handlers/common.rs
|
// src/modes/canvas/common.rs
|
||||||
|
|
||||||
|
use crossterm::event::{KeyEvent};
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
use crate::tui::terminal::grpc_client::GrpcClient;
|
use crate::tui::terminal::grpc_client::GrpcClient;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
|
use crate::tui::controls::commands::CommandHandler;
|
||||||
|
use crate::state::pages::form::FormState;
|
||||||
|
use crate::state::state::AppState;
|
||||||
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
|
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
|
||||||
|
|
||||||
|
/// Main handler for common core actions
|
||||||
|
pub async fn handle_core_action(
|
||||||
|
action: &str,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
command_handler: &mut CommandHandler,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
current_position: &mut u64,
|
||||||
|
total_count: u64,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
|
match action {
|
||||||
|
"save" => {
|
||||||
|
let message = save(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
Ok((false, message))
|
||||||
|
},
|
||||||
|
"force_quit" => {
|
||||||
|
terminal.cleanup()?;
|
||||||
|
Ok((true, "Force exiting without saving.".to_string()))
|
||||||
|
},
|
||||||
|
"save_and_quit" => {
|
||||||
|
let message = save(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
terminal.cleanup()?;
|
||||||
|
Ok((true, format!("{}. Exiting application.", message)))
|
||||||
|
},
|
||||||
|
"revert" => {
|
||||||
|
let message = revert(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
Ok((false, message))
|
||||||
|
},
|
||||||
|
// We should never hit this case with proper filtering
|
||||||
|
_ => Ok((false, format!("Core action not handled: {}", action))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to check if a key event should trigger a core action
|
||||||
|
pub fn is_core_action(config: &Config, key_code: crossterm::event::KeyCode, modifiers: crossterm::event::KeyModifiers) -> Option<String> {
|
||||||
|
// Check for core application actions (save, quit, etc.)
|
||||||
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
|
&config.keybindings.common,
|
||||||
|
key_code,
|
||||||
|
modifiers
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
|
return Some(action.to_string())
|
||||||
|
},
|
||||||
|
_ => {} // Other actions are handled by their respective mode handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared logic for saving the current form state
|
/// Shared logic for saving the current form state
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
@@ -65,64 +139,6 @@ pub async fn save(
|
|||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared logic for force quitting the application
|
|
||||||
pub fn force_quit() -> (bool, String) {
|
|
||||||
(true, "Force quitting application".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared logic for saving and quitting
|
|
||||||
pub async fn save_and_quit(
|
|
||||||
form_state: &mut FormState,
|
|
||||||
grpc_client: &mut GrpcClient,
|
|
||||||
current_position: &mut u64,
|
|
||||||
total_count: u64,
|
|
||||||
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
|
||||||
let is_new = *current_position == total_count + 1;
|
|
||||||
|
|
||||||
if is_new {
|
|
||||||
let post_request = PostAdresarRequest {
|
|
||||||
firma: form_state.values[0].clone(),
|
|
||||||
kz: form_state.values[1].clone(),
|
|
||||||
drc: form_state.values[2].clone(),
|
|
||||||
ulica: form_state.values[3].clone(),
|
|
||||||
psc: form_state.values[4].clone(),
|
|
||||||
mesto: form_state.values[5].clone(),
|
|
||||||
stat: form_state.values[6].clone(),
|
|
||||||
banka: form_state.values[7].clone(),
|
|
||||||
ucet: form_state.values[8].clone(),
|
|
||||||
skladm: form_state.values[9].clone(),
|
|
||||||
ico: form_state.values[10].clone(),
|
|
||||||
kontakt: form_state.values[11].clone(),
|
|
||||||
telefon: form_state.values[12].clone(),
|
|
||||||
skladu: form_state.values[13].clone(),
|
|
||||||
fax: form_state.values[14].clone(),
|
|
||||||
};
|
|
||||||
let _ = grpc_client.post_adresar(post_request).await?;
|
|
||||||
} else {
|
|
||||||
let put_request = PutAdresarRequest {
|
|
||||||
id: form_state.id,
|
|
||||||
firma: form_state.values[0].clone(),
|
|
||||||
kz: form_state.values[1].clone(),
|
|
||||||
drc: form_state.values[2].clone(),
|
|
||||||
ulica: form_state.values[3].clone(),
|
|
||||||
psc: form_state.values[4].clone(),
|
|
||||||
mesto: form_state.values[5].clone(),
|
|
||||||
stat: form_state.values[6].clone(),
|
|
||||||
banka: form_state.values[7].clone(),
|
|
||||||
ucet: form_state.values[8].clone(),
|
|
||||||
skladm: form_state.values[9].clone(),
|
|
||||||
ico: form_state.values[10].clone(),
|
|
||||||
kontakt: form_state.values[11].clone(),
|
|
||||||
telefon: form_state.values[12].clone(),
|
|
||||||
skladu: form_state.values[13].clone(),
|
|
||||||
fax: form_state.values[14].clone(),
|
|
||||||
};
|
|
||||||
let _ = grpc_client.put_adresar(put_request).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((true, "Saved and exiting application".to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Discard changes since last save
|
/// Discard changes since last save
|
||||||
pub async fn revert(
|
pub async fn revert(
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
// src/modes/handlers/edit.rs
|
// src/modes/canvas/edit.rs
|
||||||
|
|
||||||
|
// TODO THIS is freaking bloated with functions it never uses REFACTOR 200 LOC can be gone
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::tui::terminal::{
|
use crate::tui::terminal::{
|
||||||
grpc_client::GrpcClient,
|
grpc_client::GrpcClient,
|
||||||
};
|
};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use super::common;
|
use crate::modes::canvas::common;
|
||||||
|
|
||||||
pub async fn handle_edit_event_internal(
|
pub async fn handle_edit_event_internal(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -19,6 +20,24 @@ pub async fn handle_edit_event_internal(
|
|||||||
total_count: u64,
|
total_count: u64,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(&config.keybindings.global, key.code, key.modifiers) {
|
||||||
|
// Ignore in edit mode and process as normal input
|
||||||
|
handle_edit_specific_input(key, form_state, ideal_cursor_column);
|
||||||
|
return Ok(command_message.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check common actions first
|
||||||
|
if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.common, key.code, key.modifiers) {
|
||||||
|
return execute_common_action(
|
||||||
|
action,
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
|
||||||
return execute_edit_action(
|
return execute_edit_action(
|
||||||
action,
|
action,
|
||||||
@@ -40,6 +59,48 @@ pub async fn handle_edit_event_internal(
|
|||||||
Ok(command_message.clone())
|
Ok(command_message.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_common_action(
|
||||||
|
action: &str,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
is_saved: &mut bool,
|
||||||
|
current_position: &mut u64,
|
||||||
|
total_count: u64,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
match action {
|
||||||
|
"save" => {
|
||||||
|
common::save(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await
|
||||||
|
},
|
||||||
|
"revert" => {
|
||||||
|
common::revert(
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await
|
||||||
|
},
|
||||||
|
"move_up" | "move_down" => {
|
||||||
|
// Reuse edit mode's existing logic
|
||||||
|
execute_edit_action(
|
||||||
|
action,
|
||||||
|
form_state,
|
||||||
|
&mut 0, // Dummy ideal_cursor_column (not used here)
|
||||||
|
grpc_client,
|
||||||
|
is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await
|
||||||
|
},
|
||||||
|
_ => Ok(format!("Common action not handled: {}", action)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_edit_specific_input(
|
fn handle_edit_specific_input(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
form_state: &mut FormState,
|
form_state: &mut FormState,
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// src/modes/handlers/read_only.rs
|
// src/modes/handlers/read_only.rs
|
||||||
|
|
||||||
use crossterm::event::{KeyEvent};
|
use crossterm::event::{KeyEvent};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::config::key_sequences::KeySequenceTracker;
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
use crate::tui::terminal::grpc_client::GrpcClient;
|
use crate::tui::terminal::grpc_client::GrpcClient;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
3
client/src/modes/common.rs
Normal file
3
client/src/modes/common.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/client/modes/common.rs
|
||||||
|
pub mod command_mode;
|
||||||
|
pub mod highlight;
|
||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
||||||
use crate::tui::terminal::grpc_client::GrpcClient;
|
use crate::tui::terminal::grpc_client::GrpcClient;
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use super::common;
|
use crate::tui::controls::commands::CommandHandler;
|
||||||
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
|
use crate::modes::{
|
||||||
|
canvas::{common},
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn handle_command_event(
|
pub async fn handle_command_event(
|
||||||
key: KeyEvent,
|
key: KeyEvent,
|
||||||
@@ -13,7 +17,8 @@ pub async fn handle_command_event(
|
|||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
is_saved: &mut bool,
|
command_handler: &mut CommandHandler,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
||||||
@@ -35,7 +40,8 @@ pub async fn handle_command_event(
|
|||||||
command_input,
|
command_input,
|
||||||
command_message,
|
command_message,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
is_saved,
|
command_handler,
|
||||||
|
terminal,
|
||||||
current_position,
|
current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await;
|
).await;
|
||||||
@@ -66,7 +72,8 @@ async fn process_command(
|
|||||||
command_input: &mut String,
|
command_input: &mut String,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
grpc_client: &mut GrpcClient,
|
grpc_client: &mut GrpcClient,
|
||||||
is_saved: &mut bool,
|
command_handler: &mut CommandHandler,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
|
||||||
@@ -82,32 +89,24 @@ async fn process_command(
|
|||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
|
"force_quit" | "save_and_quit" | "quit" => {
|
||||||
|
let (should_exit, message) = command_handler
|
||||||
|
.handle_command(action, terminal)
|
||||||
|
.await?;
|
||||||
|
command_input.clear();
|
||||||
|
Ok((should_exit, message, true))
|
||||||
|
},
|
||||||
"save" => {
|
"save" => {
|
||||||
let message = common::save(
|
let message = common::save(
|
||||||
form_state,
|
form_state,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
is_saved,
|
&mut command_handler.is_saved,
|
||||||
current_position,
|
current_position,
|
||||||
total_count,
|
total_count,
|
||||||
).await?;
|
).await?;
|
||||||
command_input.clear();
|
command_input.clear();
|
||||||
return Ok((false, message, true));
|
return Ok((false, message, true));
|
||||||
},
|
},
|
||||||
"force_quit" => {
|
|
||||||
let (should_exit, message) = common::force_quit();
|
|
||||||
command_input.clear();
|
|
||||||
return Ok((should_exit, message, true));
|
|
||||||
},
|
|
||||||
"save_and_quit" => {
|
|
||||||
let (should_exit, message) = common::save_and_quit(
|
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
command_input.clear();
|
|
||||||
return Ok((should_exit, message, true));
|
|
||||||
},
|
|
||||||
"revert" => {
|
"revert" => {
|
||||||
let message = common::revert(
|
let message = common::revert(
|
||||||
form_state,
|
form_state,
|
||||||
2
client/src/modes/general.rs
Normal file
2
client/src/modes/general.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// src/client/modes/general.rs
|
||||||
|
pub mod navigation;
|
||||||
175
client/src/modes/general/navigation.rs
Normal file
175
client/src/modes/general/navigation.rs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// src/modes/general/navigation.rs
|
||||||
|
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::state::state::AppState;
|
||||||
|
use crate::state::pages::form::FormState;
|
||||||
|
|
||||||
|
pub async fn handle_navigation_event(
|
||||||
|
key: KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
form_state: &mut FormState,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
command_mode: &mut bool,
|
||||||
|
command_input: &mut String,
|
||||||
|
command_message: &mut String,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
|
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
|
||||||
|
match action {
|
||||||
|
"move_up" => {
|
||||||
|
move_up(app_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"move_down" => {
|
||||||
|
let item_count = if app_state.ui.show_intro {
|
||||||
|
2 // Intro options count
|
||||||
|
} else {
|
||||||
|
app_state.profile_tree.profiles.len() // Admin panel items
|
||||||
|
};
|
||||||
|
move_down(app_state, item_count);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"next_option" => {
|
||||||
|
next_option(app_state, 2); // Intro has 2 options
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"previous_option" => {
|
||||||
|
previous_option(app_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"select" => {
|
||||||
|
select(app_state);
|
||||||
|
return Ok((false, "Selected".to_string()));
|
||||||
|
}
|
||||||
|
"toggle_sidebar" => {
|
||||||
|
toggle_sidebar(app_state);
|
||||||
|
return Ok((false, format!("Sidebar {}",
|
||||||
|
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
"next_field" => {
|
||||||
|
next_field(form_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"prev_field" => {
|
||||||
|
prev_field(form_state);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
"enter_command_mode" => {
|
||||||
|
handle_enter_command_mode(command_mode, command_input, command_message);
|
||||||
|
return Ok((false, String::new()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((false, String::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_up(app_state: &mut AppState) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.previous_option();
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Assuming profile_tree.profiles is the list we're navigating
|
||||||
|
let profile_count = app_state.profile_tree.profiles.len();
|
||||||
|
if profile_count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use general state for tracking selection in admin panel
|
||||||
|
if app_state.general.selected_item == 0 {
|
||||||
|
app_state.general.selected_item = profile_count - 1;
|
||||||
|
} else {
|
||||||
|
app_state.general.selected_item = app_state.general.selected_item.saturating_sub(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_down(app_state: &mut AppState, item_count: usize) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.next_option();
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Assuming profile_tree.profiles is the list we're navigating
|
||||||
|
let profile_count = app_state.profile_tree.profiles.len();
|
||||||
|
if profile_count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_state.general.selected_item = (app_state.general.selected_item + 1) % profile_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_option(app_state: &mut AppState, option_count: usize) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.next_option();
|
||||||
|
} else {
|
||||||
|
// For other screens that might have options
|
||||||
|
app_state.general.current_option = (app_state.general.current_option + 1) % option_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_option(app_state: &mut AppState) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
app_state.ui.intro_state.previous_option();
|
||||||
|
} else {
|
||||||
|
// For other screens that might have options
|
||||||
|
if app_state.general.current_option == 0 {
|
||||||
|
// We'd need the option count here, but since it's not passed we can't wrap around correctly
|
||||||
|
// For now, just stay at 0
|
||||||
|
} else {
|
||||||
|
app_state.general.current_option -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(app_state: &mut AppState) {
|
||||||
|
if app_state.ui.show_intro {
|
||||||
|
// Handle selection in intro screen
|
||||||
|
if app_state.ui.intro_state.selected_option == 0 {
|
||||||
|
// First option selected - show form
|
||||||
|
app_state.ui.show_form = true;
|
||||||
|
app_state.ui.show_admin = false;
|
||||||
|
} else {
|
||||||
|
// Second option selected - show admin
|
||||||
|
app_state.ui.show_form = false;
|
||||||
|
app_state.ui.show_admin = true;
|
||||||
|
}
|
||||||
|
app_state.ui.show_intro = false;
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Handle selection in admin panel
|
||||||
|
let profiles = &app_state.profile_tree.profiles;
|
||||||
|
if !profiles.is_empty() && app_state.general.selected_item < profiles.len() {
|
||||||
|
// Set the selected profile
|
||||||
|
app_state.selected_profile = Some(profiles[app_state.general.selected_item].name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_sidebar(app_state: &mut AppState) {
|
||||||
|
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_field(form_state: &mut FormState) {
|
||||||
|
if !form_state.fields.is_empty() {
|
||||||
|
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_field(form_state: &mut FormState) {
|
||||||
|
if !form_state.fields.is_empty() {
|
||||||
|
if form_state.current_field == 0 {
|
||||||
|
form_state.current_field = form_state.fields.len() - 1;
|
||||||
|
} else {
|
||||||
|
form_state.current_field -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_enter_command_mode(
|
||||||
|
command_mode: &mut bool,
|
||||||
|
command_input: &mut String,
|
||||||
|
command_message: &mut String
|
||||||
|
) {
|
||||||
|
*command_mode = true;
|
||||||
|
command_input.clear();
|
||||||
|
command_message.clear();
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
// src/client/modes/handlers.rs
|
// src/client/modes/handlers.rs
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod edit;
|
pub mod mode_manager;
|
||||||
pub mod common;
|
|
||||||
pub mod command_mode;
|
|
||||||
pub mod read_only;
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
// src/modes/handlers/event.rs
|
// src/modes/handlers/event.rs
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyEvent};
|
||||||
use crossterm::cursor::SetCursorStyle;
|
use crossterm::cursor::SetCursorStyle;
|
||||||
use crate::tui::terminal::{
|
use crate::tui::terminal::{
|
||||||
core::TerminalCore,
|
core::TerminalCore,
|
||||||
grpc_client::GrpcClient,
|
grpc_client::GrpcClient,
|
||||||
commands::CommandHandler,
|
|
||||||
};
|
};
|
||||||
use crate::config::config::Config;
|
use crate::tui::controls::commands::CommandHandler;
|
||||||
use crate::ui::handlers::form::FormState;
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::state::pages::form::FormState;
|
||||||
use crate::ui::handlers::rat_state::UiStateHandler;
|
use crate::ui::handlers::rat_state::UiStateHandler;
|
||||||
use crate::modes::handlers::{edit, command_mode, read_only};
|
use crate::modes::{
|
||||||
use crate::config::key_sequences::KeySequenceTracker;
|
common::{command_mode},
|
||||||
use super::common;
|
canvas::{edit, read_only, common},
|
||||||
|
general::navigation,
|
||||||
|
};
|
||||||
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
|
use crate::modes::handlers::mode_manager::{ModeManager, AppMode};
|
||||||
|
|
||||||
pub struct EventHandler {
|
pub struct EventHandler {
|
||||||
pub command_mode: bool,
|
pub command_mode: bool,
|
||||||
@@ -47,179 +51,198 @@ impl EventHandler {
|
|||||||
app_state: &mut crate::state::state::AppState,
|
app_state: &mut crate::state::state::AppState,
|
||||||
total_count: u64,
|
total_count: u64,
|
||||||
current_position: &mut u64,
|
current_position: &mut u64,
|
||||||
intro_state: &mut crate::components::handlers::intro::IntroState,
|
|
||||||
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
if app_state.ui.show_intro {
|
// Determine current mode based on app state and event handler state
|
||||||
if let Event::Key(key) = event {
|
let current_mode = ModeManager::derive_mode(app_state, self);
|
||||||
match key.code {
|
app_state.update_mode(current_mode);
|
||||||
KeyCode::Left => intro_state.previous_option(),
|
|
||||||
KeyCode::Right => intro_state.next_option(),
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if intro_state.selected_option == 0 {
|
|
||||||
app_state.ui.show_intro = false;
|
|
||||||
} else {
|
|
||||||
self.command_message = "Admin panel coming soon".to_string();
|
|
||||||
}
|
|
||||||
return Ok((false, String::new()));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok((false, String::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Event::Key(key) = event {
|
if let Event::Key(key) = event {
|
||||||
let key_code = key.code;
|
let key_code = key.code;
|
||||||
let modifiers = key.modifiers;
|
let modifiers = key.modifiers;
|
||||||
|
|
||||||
if UiStateHandler::toggle_sidebar(
|
// Handle common actions across all modes
|
||||||
&mut app_state.ui,
|
if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) {
|
||||||
config,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
return Ok((false, format!("Sidebar {}",
|
return Ok((false, format!("Sidebar {}",
|
||||||
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
|
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
// Mode-specific handling
|
||||||
&config.keybindings.common,
|
match current_mode {
|
||||||
key_code,
|
AppMode::General => {
|
||||||
modifiers
|
return navigation::handle_navigation_event(
|
||||||
) {
|
key,
|
||||||
match action {
|
config,
|
||||||
"save" => {
|
form_state,
|
||||||
let message = common::save(
|
app_state,
|
||||||
form_state,
|
&mut self.command_mode,
|
||||||
grpc_client,
|
&mut self.command_input,
|
||||||
&mut app_state.ui.is_saved,
|
&mut self.command_message,
|
||||||
current_position,
|
).await;
|
||||||
total_count,
|
},
|
||||||
).await?;
|
|
||||||
return Ok((false, message));
|
|
||||||
},
|
|
||||||
"force_quit" => {
|
|
||||||
let (should_exit, message) = command_handler.handle_command("force_quit", terminal).await?;
|
|
||||||
return Ok((should_exit, message));
|
|
||||||
},
|
|
||||||
"save_and_quit" => {
|
|
||||||
let (should_exit, message) = command_handler.handle_command("save_and_quit", terminal).await?;
|
|
||||||
return Ok((should_exit, message));
|
|
||||||
},
|
|
||||||
"revert" => {
|
|
||||||
let message = common::revert(
|
|
||||||
form_state,
|
|
||||||
grpc_client,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
return Ok((false, message));
|
|
||||||
},
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.command_mode {
|
AppMode::ReadOnly => {
|
||||||
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event(
|
// Check for mode transitions first
|
||||||
key,
|
if config.is_enter_edit_mode_before(key_code, modifiers) &&
|
||||||
config,
|
ModeManager::can_enter_edit_mode(current_mode) {
|
||||||
form_state,
|
self.is_edit_mode = true;
|
||||||
&mut self.command_input,
|
self.edit_mode_cooldown = true;
|
||||||
&mut self.command_message,
|
self.command_message = "Edit mode".to_string();
|
||||||
grpc_client,
|
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||||
&mut app_state.ui.is_saved,
|
|
||||||
current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
if exit_command_mode {
|
|
||||||
self.command_mode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok((should_exit, message));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_edit_mode {
|
|
||||||
if config.is_exit_edit_mode(key_code, modifiers) {
|
|
||||||
if form_state.has_unsaved_changes {
|
|
||||||
self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string();
|
|
||||||
return Ok((false, self.command_message.clone()));
|
return Ok((false, self.command_message.clone()));
|
||||||
}
|
}
|
||||||
self.is_edit_mode = false;
|
|
||||||
self.edit_mode_cooldown = true;
|
|
||||||
self.command_message = "Read-only mode".to_string();
|
|
||||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
|
||||||
|
|
||||||
let current_input = form_state.get_current_input();
|
if config.is_enter_edit_mode_after(key_code, modifiers) &&
|
||||||
if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() {
|
ModeManager::can_enter_edit_mode(current_mode) {
|
||||||
form_state.current_cursor_pos = current_input.len() - 1;
|
let current_input = form_state.get_current_input();
|
||||||
self.ideal_cursor_column = form_state.current_cursor_pos;
|
if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() {
|
||||||
|
form_state.current_cursor_pos += 1;
|
||||||
|
self.ideal_cursor_column = form_state.current_cursor_pos;
|
||||||
|
}
|
||||||
|
self.is_edit_mode = true;
|
||||||
|
self.edit_mode_cooldown = true;
|
||||||
|
self.command_message = "Edit mode (after cursor)".to_string();
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||||
|
return Ok((false, self.command_message.clone()));
|
||||||
}
|
}
|
||||||
return Ok((false, self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = edit::handle_edit_event_internal(
|
// Check for entering command mode
|
||||||
key,
|
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
|
||||||
config,
|
if action == "enter_command_mode" && ModeManager::can_enter_command_mode(current_mode) {
|
||||||
form_state,
|
self.command_mode = true;
|
||||||
&mut self.ideal_cursor_column,
|
self.command_input.clear();
|
||||||
&mut self.command_message,
|
self.command_message.clear();
|
||||||
&mut app_state.ui.is_saved,
|
return Ok((false, String::new()));
|
||||||
current_position,
|
}
|
||||||
total_count,
|
|
||||||
grpc_client,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
return Ok((false, result));
|
|
||||||
} else {
|
|
||||||
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
|
|
||||||
if action == "enter_command_mode" {
|
|
||||||
self.command_mode = true;
|
|
||||||
self.command_input.clear();
|
|
||||||
self.command_message.clear();
|
|
||||||
return Ok((false, String::new()));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_enter_edit_mode_before(key_code, modifiers) {
|
// Check for core application actions (save, quit, etc.)
|
||||||
self.is_edit_mode = true;
|
// ONLY handle a limited subset of core actions here
|
||||||
self.edit_mode_cooldown = true;
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
self.command_message = "Edit mode".to_string();
|
&config.keybindings.common,
|
||||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
key_code,
|
||||||
return Ok((false, self.command_message.clone()));
|
modifiers
|
||||||
}
|
) {
|
||||||
|
match action {
|
||||||
if config.is_enter_edit_mode_after(key_code, modifiers) {
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
let current_input = form_state.get_current_input();
|
return common::handle_core_action(
|
||||||
if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() {
|
action,
|
||||||
form_state.current_cursor_pos += 1;
|
form_state,
|
||||||
self.ideal_cursor_column = form_state.current_cursor_pos;
|
grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
app_state,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
_ => {} // For other actions, let the mode-specific handler take care of it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.is_edit_mode = true;
|
|
||||||
self.edit_mode_cooldown = true;
|
|
||||||
self.command_message = "Edit mode (after cursor)".to_string();
|
|
||||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
|
||||||
return Ok((false, self.command_message.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return read_only::handle_read_only_event(
|
// Let read_only mode handle its own actions (including navigation from common bindings)
|
||||||
key,
|
return read_only::handle_read_only_event(
|
||||||
config,
|
key,
|
||||||
form_state,
|
config,
|
||||||
&mut self.key_sequence_tracker,
|
form_state,
|
||||||
current_position,
|
&mut self.key_sequence_tracker,
|
||||||
total_count,
|
current_position,
|
||||||
grpc_client,
|
total_count,
|
||||||
&mut self.command_message,
|
grpc_client,
|
||||||
&mut self.edit_mode_cooldown,
|
&mut self.command_message,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.edit_mode_cooldown,
|
||||||
).await;
|
&mut self.ideal_cursor_column,
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
|
||||||
|
AppMode::Edit => {
|
||||||
|
// Check for exiting edit mode
|
||||||
|
if config.is_exit_edit_mode(key_code, modifiers) {
|
||||||
|
if form_state.has_unsaved_changes {
|
||||||
|
self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string();
|
||||||
|
return Ok((false, self.command_message.clone()));
|
||||||
|
}
|
||||||
|
self.is_edit_mode = false;
|
||||||
|
self.edit_mode_cooldown = true;
|
||||||
|
self.command_message = "Read-only mode".to_string();
|
||||||
|
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||||
|
|
||||||
|
let current_input = form_state.get_current_input();
|
||||||
|
if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() {
|
||||||
|
form_state.current_cursor_pos = current_input.len() - 1;
|
||||||
|
self.ideal_cursor_column = form_state.current_cursor_pos;
|
||||||
|
}
|
||||||
|
return Ok((false, self.command_message.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for core application actions (save, quit, etc.)
|
||||||
|
// ONLY handle a limited subset of core actions here
|
||||||
|
if let Some(action) = config.get_action_for_key_in_mode(
|
||||||
|
&config.keybindings.common,
|
||||||
|
key_code,
|
||||||
|
modifiers
|
||||||
|
) {
|
||||||
|
match action {
|
||||||
|
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||||
|
return common::handle_core_action(
|
||||||
|
action,
|
||||||
|
form_state,
|
||||||
|
grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
app_state,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await;
|
||||||
|
},
|
||||||
|
_ => {} // For other actions, let the mode-specific handler take care of it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let edit mode handle its own actions (including navigation from common bindings)
|
||||||
|
let result = edit::handle_edit_event_internal(
|
||||||
|
key,
|
||||||
|
config,
|
||||||
|
form_state,
|
||||||
|
&mut self.ideal_cursor_column,
|
||||||
|
&mut self.command_message,
|
||||||
|
&mut app_state.ui.is_saved,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
grpc_client,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
self.key_sequence_tracker.reset();
|
||||||
|
return Ok((false, result));
|
||||||
|
},
|
||||||
|
|
||||||
|
AppMode::Command => {
|
||||||
|
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event(
|
||||||
|
key,
|
||||||
|
config,
|
||||||
|
form_state,
|
||||||
|
&mut self.command_input,
|
||||||
|
&mut self.command_message,
|
||||||
|
grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
current_position,
|
||||||
|
total_count,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
if exit_command_mode {
|
||||||
|
self.command_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok((should_exit, message));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-key events or if no specific handler was matched
|
||||||
self.edit_mode_cooldown = false;
|
self.edit_mode_cooldown = false;
|
||||||
Ok((false, self.command_message.clone()))
|
Ok((false, self.command_message.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
50
client/src/modes/handlers/mode_manager.rs
Normal file
50
client/src/modes/handlers/mode_manager.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// src/modes/handlers/mode_manager.rs
|
||||||
|
use crate::state::state::AppState;
|
||||||
|
use crate::modes::handlers::event::EventHandler;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AppMode {
|
||||||
|
General, // For intro and admin screens
|
||||||
|
ReadOnly, // Canvas read-only mode
|
||||||
|
Edit, // Canvas edit mode
|
||||||
|
Command, // Command mode overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModeManager;
|
||||||
|
|
||||||
|
impl ModeManager {
|
||||||
|
// Determine current mode based on app state
|
||||||
|
pub fn derive_mode(app_state: &AppState, event_handler: &EventHandler) -> AppMode {
|
||||||
|
// Command mode takes precedence if active
|
||||||
|
if event_handler.command_mode {
|
||||||
|
return AppMode::Command;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check UI state flags
|
||||||
|
if app_state.ui.show_intro || app_state.ui.show_admin {
|
||||||
|
AppMode::General
|
||||||
|
} else if app_state.ui.show_form {
|
||||||
|
if event_handler.is_edit_mode {
|
||||||
|
AppMode::Edit
|
||||||
|
} else {
|
||||||
|
AppMode::ReadOnly
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
AppMode::General
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode transition rules
|
||||||
|
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
||||||
|
!matches!(current_mode, AppMode::Edit) // Can't enter from Edit mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
||||||
|
matches!(current_mode, AppMode::ReadOnly) // Only from ReadOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
||||||
|
matches!(current_mode, AppMode::Edit | AppMode::Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
// src/client/modes/mod.rs
|
// src/client/modes/mod.rs
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod canvas;
|
||||||
|
pub mod general;
|
||||||
|
pub mod common;
|
||||||
|
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
|
pub use canvas::*;
|
||||||
|
pub use general::*;
|
||||||
|
pub use common::*;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
// src/state/mod.rs
|
// src/state/mod.rs
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod pages;
|
||||||
|
|||||||
3
client/src/state/pages.rs
Normal file
3
client/src/state/pages.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/state/pages.rs
|
||||||
|
|
||||||
|
pub mod form;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/client/ui/handlers/form.rs
|
// src/state/pages/form.rs
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ impl FormState {
|
|||||||
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect();
|
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect();
|
||||||
let values: Vec<&String> = self.values.iter().collect();
|
let values: Vec<&String> = self.values.iter().collect();
|
||||||
|
|
||||||
crate::components::handlers::form::render_form(
|
crate::components::form::form::render_form(
|
||||||
f,
|
f,
|
||||||
area,
|
area,
|
||||||
self,
|
self,
|
||||||
@@ -1,12 +1,23 @@
|
|||||||
// src/client/ui/handlers/state.rs
|
// src/state/state.rs
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||||
|
use crate::components::IntroState;
|
||||||
|
use crate::modes::handlers::mode_manager::AppMode;
|
||||||
|
|
||||||
pub struct UiState {
|
pub struct UiState {
|
||||||
pub show_sidebar: bool,
|
pub show_sidebar: bool,
|
||||||
pub is_saved: bool,
|
pub is_saved: bool,
|
||||||
pub show_intro: bool,
|
pub show_intro: bool,
|
||||||
|
pub show_admin: bool,
|
||||||
|
pub show_form: bool,
|
||||||
|
pub show_login: bool,
|
||||||
|
pub intro_state: IntroState,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GeneralState {
|
||||||
|
pub selected_item: usize,
|
||||||
|
pub current_option: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@@ -15,9 +26,12 @@ pub struct AppState {
|
|||||||
pub total_count: u64,
|
pub total_count: u64,
|
||||||
pub current_position: u64,
|
pub current_position: u64,
|
||||||
pub profile_tree: ProfileTreeResponse,
|
pub profile_tree: ProfileTreeResponse,
|
||||||
|
pub selected_profile: Option<String>,
|
||||||
|
pub current_mode: AppMode,
|
||||||
|
|
||||||
// UI preferences
|
// UI preferences
|
||||||
pub ui: UiState,
|
pub ui: UiState,
|
||||||
|
pub general: GeneralState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -30,7 +44,13 @@ impl AppState {
|
|||||||
total_count: 0,
|
total_count: 0,
|
||||||
current_position: 0,
|
current_position: 0,
|
||||||
profile_tree: ProfileTreeResponse::default(),
|
profile_tree: ProfileTreeResponse::default(),
|
||||||
|
selected_profile: None,
|
||||||
|
current_mode: AppMode::General,
|
||||||
ui: UiState::default(),
|
ui: UiState::default(),
|
||||||
|
general: GeneralState {
|
||||||
|
selected_item: 0,
|
||||||
|
current_option: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +62,10 @@ impl AppState {
|
|||||||
pub fn update_current_position(&mut self, current_position: u64) {
|
pub fn update_current_position(&mut self, current_position: u64) {
|
||||||
self.current_position = current_position;
|
self.current_position = current_position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_mode(&mut self, mode: AppMode) {
|
||||||
|
self.current_mode = mode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -50,6 +74,10 @@ impl Default for UiState {
|
|||||||
show_sidebar: true,
|
show_sidebar: true,
|
||||||
is_saved: false,
|
is_saved: false,
|
||||||
show_intro: true,
|
show_intro: true,
|
||||||
|
show_admin: false,
|
||||||
|
show_form: false,
|
||||||
|
show_login: false,
|
||||||
|
intro_state: IntroState::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
client/src/tui/controls.rs
Normal file
5
client/src/tui/controls.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// src/tui/controls.rs
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
|
pub use commands::*;
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// src/tui/terminal/commands.rs
|
// src/tui/controls/commands.rs
|
||||||
|
|
||||||
use crate::tui::terminal::core::TerminalCore;
|
use crate::tui::terminal::core::TerminalCore;
|
||||||
|
|
||||||
pub struct CommandHandler {
|
pub struct CommandHandler {
|
||||||
is_saved: bool,
|
pub is_saved: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandHandler {
|
impl CommandHandler {
|
||||||
@@ -24,7 +23,10 @@ impl CommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_quit(&self, terminal: &mut TerminalCore) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
async fn handle_quit(
|
||||||
|
&self,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
if self.is_saved {
|
if self.is_saved {
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok((true, "Exiting.".into()))
|
Ok((true, "Exiting.".into()))
|
||||||
@@ -33,12 +35,18 @@ impl CommandHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_force_quit(&self, terminal: &mut TerminalCore) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
async fn handle_force_quit(
|
||||||
|
&self,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok((true, "Force exiting without saving.".into()))
|
Ok((true, "Force exiting without saving.".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_save_quit(&mut self, terminal: &mut TerminalCore) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
async fn handle_save_quit(
|
||||||
|
&mut self,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
) -> Result<(bool, String), Box<dyn std::error::Error>> {
|
||||||
self.is_saved = true;
|
self.is_saved = true;
|
||||||
terminal.cleanup()?;
|
terminal.cleanup()?;
|
||||||
Ok((true, "State saved. Exiting.".into()))
|
Ok((true, "State saved. Exiting.".into()))
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
// src/tui/mod.rs
|
// src/tui/mod.rs
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
pub mod controls;
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
|
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod grpc_client;
|
pub mod grpc_client;
|
||||||
pub mod commands;
|
|
||||||
pub mod event_reader;
|
pub mod event_reader;
|
||||||
|
|
||||||
pub use core::TerminalCore;
|
pub use core::TerminalCore;
|
||||||
pub use grpc_client::GrpcClient;
|
pub use grpc_client::GrpcClient;
|
||||||
pub use commands::CommandHandler;
|
|
||||||
pub use event_reader::EventReader;
|
pub use event_reader::EventReader;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// src/client/ui/handlers.rs
|
// src/client/ui/handlers.rs
|
||||||
|
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod form;
|
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod rat_state;
|
pub mod rat_state;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/ui/handlers/rat_state.rs
|
// src/ui/handlers/rat_state.rs
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::state::state::UiState;
|
use crate::state::state::UiState;
|
||||||
|
|
||||||
pub struct UiStateHandler;
|
pub struct UiStateHandler;
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
// src/ui/handlers/render.rs
|
// src/ui/handlers/render.rs
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
render_background,
|
render_background,
|
||||||
render_command_line,
|
render_command_line,
|
||||||
render_status_line,
|
render_status_line,
|
||||||
handlers::{sidebar::{self, calculate_sidebar_layout}, intro},
|
handlers::sidebar::{self, calculate_sidebar_layout},
|
||||||
|
form::form::render_form,
|
||||||
|
intro::{intro},
|
||||||
|
admin::{admin_panel::AdminPanelState},
|
||||||
};
|
};
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use ratatui::layout::{Constraint, Direction, Layout};
|
use ratatui::layout::{Constraint, Direction, Layout};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use super::form::FormState;
|
use crate::state::pages::form::FormState;
|
||||||
use crate::state::state::AppState;
|
use crate::state::state::AppState;
|
||||||
|
|
||||||
pub fn render_ui(
|
pub fn render_ui(
|
||||||
@@ -23,59 +27,106 @@ pub fn render_ui(
|
|||||||
command_mode: bool,
|
command_mode: bool,
|
||||||
command_message: &str,
|
command_message: &str,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
intro_state: &intro::IntroState,
|
// intro_state parameter removed
|
||||||
) {
|
) {
|
||||||
render_background(f, f.area(), theme);
|
render_background(f, f.area(), theme);
|
||||||
|
|
||||||
if app_state.ui.show_intro {
|
|
||||||
intro_state.render(f, f.area(), theme);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let root = Layout::default()
|
let root = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Min(10),
|
Constraint::Min(1),
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
let main_content_area = root[0];
|
let main_content_area = root[0];
|
||||||
let (sidebar_area, form_area) = calculate_sidebar_layout(app_state.ui.show_sidebar, main_content_area);
|
if app_state.ui.show_intro {
|
||||||
let available_width = form_area.width;
|
// Use app_state's intro_state directly
|
||||||
|
app_state.ui.intro_state.render(f, main_content_area, theme);
|
||||||
|
} else if app_state.ui.show_admin {
|
||||||
|
// Create temporary AdminPanelState for rendering
|
||||||
|
let mut admin_state = AdminPanelState::new(
|
||||||
|
app_state.profile_tree.profiles
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.name.clone())
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
|
||||||
let form_constraint = if available_width >= 80 {
|
// Set the selected item - FIXED
|
||||||
Layout::default()
|
if !admin_state.profiles.is_empty() {
|
||||||
.direction(Direction::Horizontal)
|
let selected_index = std::cmp::min(
|
||||||
.constraints([
|
app_state.general.selected_item,
|
||||||
Constraint::Min(0),
|
admin_state.profiles.len() - 1
|
||||||
Constraint::Length(80),
|
);
|
||||||
Constraint::Min(0),
|
admin_state.list_state.select(Some(selected_index));
|
||||||
])
|
}
|
||||||
.split(main_content_area)[1]
|
|
||||||
} else {
|
|
||||||
Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(80.min(available_width)),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(form_area)[1]
|
|
||||||
};
|
|
||||||
|
|
||||||
form_state.render(
|
admin_state.render(
|
||||||
f,
|
f,
|
||||||
form_constraint,
|
main_content_area,
|
||||||
theme,
|
theme,
|
||||||
is_edit_mode,
|
&app_state.profile_tree,
|
||||||
total_count,
|
&app_state.selected_profile,
|
||||||
current_position,
|
);
|
||||||
);
|
} else if app_state.ui.show_form {
|
||||||
|
let (sidebar_area, form_area) = calculate_sidebar_layout(
|
||||||
|
app_state.ui.show_sidebar,
|
||||||
|
main_content_area
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(sidebar_rect) = sidebar_area {
|
||||||
|
sidebar::render_sidebar(
|
||||||
|
f,
|
||||||
|
sidebar_rect,
|
||||||
|
theme,
|
||||||
|
&app_state.profile_tree,
|
||||||
|
&app_state.selected_profile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This change makes the form stay stationary when toggling sidebar
|
||||||
|
let available_width = form_area.width;
|
||||||
|
let form_constraint = if available_width >= 80 {
|
||||||
|
// Use main_content_area for centering when enough space
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(80),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(main_content_area)[1]
|
||||||
|
} else {
|
||||||
|
// Use form_area (post sidebar) when limited space
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(80.min(available_width)),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.split(form_area)[1]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert fields to &[&str] and values to &[&String]
|
||||||
|
let fields: Vec<&str> = form_state.fields.iter().map(|s| s.as_str()).collect();
|
||||||
|
let values: Vec<&String> = form_state.values.iter().collect();
|
||||||
|
|
||||||
|
render_form(
|
||||||
|
f,
|
||||||
|
form_constraint,
|
||||||
|
form_state,
|
||||||
|
&fields,
|
||||||
|
&form_state.current_field,
|
||||||
|
&values,
|
||||||
|
theme,
|
||||||
|
is_edit_mode,
|
||||||
|
total_count,
|
||||||
|
current_position,
|
||||||
|
);
|
||||||
|
} else{
|
||||||
|
|
||||||
if let Some(sidebar_rect) = sidebar_area {
|
|
||||||
sidebar::render_sidebar(f, sidebar_rect, theme, &app_state.profile_tree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_status_line(f, root[1], current_dir, theme, is_edit_mode);
|
render_status_line(f, root[1], current_dir, theme, is_edit_mode);
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
// src/client/ui/handlers/ui.rs
|
// src/ui/handlers/ui.rs
|
||||||
|
|
||||||
use crate::tui::terminal::TerminalCore;
|
use crate::tui::terminal::TerminalCore;
|
||||||
use crate::tui::terminal::GrpcClient;
|
use crate::tui::terminal::GrpcClient;
|
||||||
use crate::tui::terminal::CommandHandler;
|
use crate::tui::controls::CommandHandler;
|
||||||
use crate::tui::terminal::EventReader;
|
use crate::tui::terminal::EventReader;
|
||||||
use crate::config::colors::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::config::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::ui::handlers::{form::FormState, render::render_ui};
|
use crate::ui::handlers::render::render_ui;
|
||||||
|
use crate::state::pages::form::FormState;
|
||||||
use crate::modes::handlers::event::EventHandler;
|
use crate::modes::handlers::event::EventHandler;
|
||||||
use crate::state::state::AppState;
|
use crate::state::state::AppState;
|
||||||
use crate::components::handlers::intro::IntroState;
|
use crate::components::admin::{admin_panel::AdminPanelState};
|
||||||
|
use crate::components::intro::{intro::IntroState};
|
||||||
|
|
||||||
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load()?;
|
let config = Config::load()?;
|
||||||
@@ -19,8 +21,21 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let theme = Theme::from_str(&config.colors.theme);
|
let theme = Theme::from_str(&config.colors.theme);
|
||||||
let mut intro_state = IntroState::new();
|
let mut intro_state = IntroState::new();
|
||||||
|
|
||||||
|
// Initialize app_state first
|
||||||
|
let mut app_state = AppState::new()?;
|
||||||
|
|
||||||
|
// Fetch profile tree and table structure
|
||||||
|
let profile_tree = grpc_client.get_profile_tree().await?;
|
||||||
|
app_state.profile_tree = profile_tree;
|
||||||
|
|
||||||
|
// Now create admin panel with profiles from app_state
|
||||||
|
if intro_state.selected_option == 1 {
|
||||||
|
app_state.ui.show_admin = true;
|
||||||
|
app_state.general.selected_item = 0;
|
||||||
|
app_state.general.current_option = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch table structure at startup (one-time)
|
// Fetch table structure at startup (one-time)
|
||||||
// TODO: Later, consider implementing a live update for table structure changes.
|
|
||||||
let table_structure = grpc_client.get_table_structure().await?;
|
let table_structure = grpc_client.get_table_structure().await?;
|
||||||
|
|
||||||
// Extract the column names from the response
|
// Extract the column names from the response
|
||||||
@@ -33,13 +48,9 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Initialize FormState with dynamic fields
|
// Initialize FormState with dynamic fields
|
||||||
let mut form_state = FormState::new(column_names);
|
let mut form_state = FormState::new(column_names);
|
||||||
|
|
||||||
// Fetch profile tree and table structure
|
|
||||||
let profile_tree = grpc_client.get_profile_tree().await?;
|
|
||||||
// The rest of your UI initialization remains the same
|
// The rest of your UI initialization remains the same
|
||||||
let mut event_handler = EventHandler::new();
|
let mut event_handler = EventHandler::new();
|
||||||
let event_reader = EventReader::new();
|
let event_reader = EventReader::new();
|
||||||
let mut app_state = AppState::new()?;
|
|
||||||
app_state.profile_tree = profile_tree;
|
|
||||||
|
|
||||||
// Fetch the total count of Adresar entries
|
// Fetch the total count of Adresar entries
|
||||||
let total_count = grpc_client.get_adresar_count().await?;
|
let total_count = grpc_client.get_adresar_count().await?;
|
||||||
@@ -64,7 +75,6 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
event_handler.command_mode,
|
event_handler.command_mode,
|
||||||
&event_handler.command_message,
|
&event_handler.command_message,
|
||||||
&app_state,
|
&app_state,
|
||||||
&intro_state,
|
|
||||||
);
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -82,7 +92,6 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
&mut app_state,
|
&mut app_state,
|
||||||
total_count,
|
total_count,
|
||||||
&mut current_position,
|
&mut current_position,
|
||||||
&mut intro_state,
|
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
app_state.current_position = current_position;
|
app_state.current_position = current_position;
|
||||||
@@ -97,7 +106,6 @@ pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
};
|
};
|
||||||
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
|
||||||
|
|
||||||
|
|
||||||
// Ensure position never exceeds total_count + 1
|
// Ensure position never exceeds total_count + 1
|
||||||
if app_state.current_position > total_count + 1 {
|
if app_state.current_position > total_count + 1 {
|
||||||
app_state.current_position = total_count + 1;
|
app_state.current_position = total_count + 1;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
&[
|
&[
|
||||||
"proto/common.proto",
|
"proto/common.proto",
|
||||||
"proto/adresar.proto",
|
"proto/adresar.proto",
|
||||||
|
"proto/auth.proto",
|
||||||
"proto/uctovnictvo.proto",
|
"proto/uctovnictvo.proto",
|
||||||
"proto/table_structure.proto",
|
"proto/table_structure.proto",
|
||||||
"proto/table_definition.proto",
|
"proto/table_definition.proto",
|
||||||
|
|||||||
37
common/proto/auth.proto
Normal file
37
common/proto/auth.proto
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// proto/auth.proto
|
||||||
|
syntax = "proto3";
|
||||||
|
package multieko2.auth;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
|
||||||
|
service AuthService {
|
||||||
|
rpc Register(RegisterRequest) returns (AuthResponse);
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterRequest {
|
||||||
|
string username = 1;
|
||||||
|
string email = 2;
|
||||||
|
string password = 3;
|
||||||
|
string password_confirmation = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthResponse {
|
||||||
|
string id = 1; // UUID in string format
|
||||||
|
string username = 2; // Registered username
|
||||||
|
string email = 3; // Registered email (if provided)
|
||||||
|
string role = 4; // Default role: 'accountant'
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string identifier = 1; // Can be username or email
|
||||||
|
string password = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string access_token = 1; // JWT token
|
||||||
|
string token_type = 2; // Usually "Bearer"
|
||||||
|
int32 expires_in = 3; // Expiration in seconds (86400 for 24 hours)
|
||||||
|
string user_id = 4; // User's UUID in string format
|
||||||
|
string role = 5; // User's role
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ pub mod proto {
|
|||||||
pub mod adresar {
|
pub mod adresar {
|
||||||
include!("proto/multieko2.adresar.rs");
|
include!("proto/multieko2.adresar.rs");
|
||||||
}
|
}
|
||||||
|
pub mod auth {
|
||||||
|
include!("proto/multieko2.auth.rs");
|
||||||
|
}
|
||||||
pub mod common {
|
pub mod common {
|
||||||
include!("proto/multieko2.common.rs");
|
include!("proto/multieko2.common.rs");
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
412
common/src/proto/multieko2.auth.rs
Normal file
412
common/src/proto/multieko2.auth.rs
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
// This file is @generated by prost-build.
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub username: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub email: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub password: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub password_confirmation: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct AuthResponse {
|
||||||
|
/// UUID in string format
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub id: ::prost::alloc::string::String,
|
||||||
|
/// Registered username
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub username: ::prost::alloc::string::String,
|
||||||
|
/// Registered email (if provided)
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub email: ::prost::alloc::string::String,
|
||||||
|
/// Default role: 'accountant'
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub role: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
/// Can be username or email
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub identifier: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub password: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
/// JWT token
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub access_token: ::prost::alloc::string::String,
|
||||||
|
/// Usually "Bearer"
|
||||||
|
#[prost(string, tag = "2")]
|
||||||
|
pub token_type: ::prost::alloc::string::String,
|
||||||
|
/// Expiration in seconds (86400 for 24 hours)
|
||||||
|
#[prost(int32, tag = "3")]
|
||||||
|
pub expires_in: i32,
|
||||||
|
/// User's UUID in string format
|
||||||
|
#[prost(string, tag = "4")]
|
||||||
|
pub user_id: ::prost::alloc::string::String,
|
||||||
|
/// User's role
|
||||||
|
#[prost(string, tag = "5")]
|
||||||
|
pub role: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// Generated client implementations.
|
||||||
|
pub mod auth_service_client {
|
||||||
|
#![allow(
|
||||||
|
unused_variables,
|
||||||
|
dead_code,
|
||||||
|
missing_docs,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
use tonic::codegen::http::Uri;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AuthServiceClient<T> {
|
||||||
|
inner: tonic::client::Grpc<T>,
|
||||||
|
}
|
||||||
|
impl AuthServiceClient<tonic::transport::Channel> {
|
||||||
|
/// Attempt to create a new client by connecting to a given endpoint.
|
||||||
|
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||||
|
where
|
||||||
|
D: TryInto<tonic::transport::Endpoint>,
|
||||||
|
D::Error: Into<StdError>,
|
||||||
|
{
|
||||||
|
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||||
|
Ok(Self::new(conn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> AuthServiceClient<T>
|
||||||
|
where
|
||||||
|
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||||
|
T::Error: Into<StdError>,
|
||||||
|
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||||
|
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||||
|
{
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::new(inner);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||||
|
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> AuthServiceClient<InterceptedService<T, F>>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
T::ResponseBody: Default,
|
||||||
|
T: tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
Response = http::Response<
|
||||||
|
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
<T as tonic::codegen::Service<
|
||||||
|
http::Request<tonic::body::BoxBody>,
|
||||||
|
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||||
|
{
|
||||||
|
AuthServiceClient::new(InterceptedService::new(inner, interceptor))
|
||||||
|
}
|
||||||
|
/// Compress requests with the given encoding.
|
||||||
|
///
|
||||||
|
/// This requires the server to support it otherwise it might respond with an
|
||||||
|
/// error.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.send_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Enable decompressing responses.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.inner = self.inner.accept_compressed(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_decoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.inner = self.inner.max_encoding_message_size(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub async fn register(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::RegisterRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::unknown(
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/multieko2.auth.AuthService/Register",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Register"));
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
pub async fn login(
|
||||||
|
&mut self,
|
||||||
|
request: impl tonic::IntoRequest<super::LoginRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status> {
|
||||||
|
self.inner
|
||||||
|
.ready()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tonic::Status::unknown(
|
||||||
|
format!("Service was not ready: {}", e.into()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
|
"/multieko2.auth.AuthService/Login",
|
||||||
|
);
|
||||||
|
let mut req = request.into_request();
|
||||||
|
req.extensions_mut()
|
||||||
|
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Login"));
|
||||||
|
self.inner.unary(req, path, codec).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated server implementations.
|
||||||
|
pub mod auth_service_server {
|
||||||
|
#![allow(
|
||||||
|
unused_variables,
|
||||||
|
dead_code,
|
||||||
|
missing_docs,
|
||||||
|
clippy::wildcard_imports,
|
||||||
|
clippy::let_unit_value,
|
||||||
|
)]
|
||||||
|
use tonic::codegen::*;
|
||||||
|
/// Generated trait containing gRPC methods that should be implemented for use with AuthServiceServer.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuthService: std::marker::Send + std::marker::Sync + 'static {
|
||||||
|
async fn register(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<super::RegisterRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status>;
|
||||||
|
async fn login(
|
||||||
|
&self,
|
||||||
|
request: tonic::Request<super::LoginRequest>,
|
||||||
|
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status>;
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AuthServiceServer<T> {
|
||||||
|
inner: Arc<T>,
|
||||||
|
accept_compression_encodings: EnabledCompressionEncodings,
|
||||||
|
send_compression_encodings: EnabledCompressionEncodings,
|
||||||
|
max_decoding_message_size: Option<usize>,
|
||||||
|
max_encoding_message_size: Option<usize>,
|
||||||
|
}
|
||||||
|
impl<T> AuthServiceServer<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self::from_arc(Arc::new(inner))
|
||||||
|
}
|
||||||
|
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
accept_compression_encodings: Default::default(),
|
||||||
|
send_compression_encodings: Default::default(),
|
||||||
|
max_decoding_message_size: None,
|
||||||
|
max_encoding_message_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_interceptor<F>(
|
||||||
|
inner: T,
|
||||||
|
interceptor: F,
|
||||||
|
) -> InterceptedService<Self, F>
|
||||||
|
where
|
||||||
|
F: tonic::service::Interceptor,
|
||||||
|
{
|
||||||
|
InterceptedService::new(Self::new(inner), interceptor)
|
||||||
|
}
|
||||||
|
/// Enable decompressing requests with the given encoding.
|
||||||
|
#[must_use]
|
||||||
|
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.accept_compression_encodings.enable(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Compress responses with the given encoding, if the client supports it.
|
||||||
|
#[must_use]
|
||||||
|
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||||
|
self.send_compression_encodings.enable(encoding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of a decoded message.
|
||||||
|
///
|
||||||
|
/// Default: `4MB`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.max_decoding_message_size = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Limits the maximum size of an encoded message.
|
||||||
|
///
|
||||||
|
/// Default: `usize::MAX`
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||||
|
self.max_encoding_message_size = Some(limit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T, B> tonic::codegen::Service<http::Request<B>> for AuthServiceServer<T>
|
||||||
|
where
|
||||||
|
T: AuthService,
|
||||||
|
B: Body + std::marker::Send + 'static,
|
||||||
|
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||||
|
{
|
||||||
|
type Response = http::Response<tonic::body::BoxBody>;
|
||||||
|
type Error = std::convert::Infallible;
|
||||||
|
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||||
|
fn poll_ready(
|
||||||
|
&mut self,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||||
|
match req.uri().path() {
|
||||||
|
"/multieko2.auth.AuthService/Register" => {
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
struct RegisterSvc<T: AuthService>(pub Arc<T>);
|
||||||
|
impl<
|
||||||
|
T: AuthService,
|
||||||
|
> tonic::server::UnaryService<super::RegisterRequest>
|
||||||
|
for RegisterSvc<T> {
|
||||||
|
type Response = super::AuthResponse;
|
||||||
|
type Future = BoxFuture<
|
||||||
|
tonic::Response<Self::Response>,
|
||||||
|
tonic::Status,
|
||||||
|
>;
|
||||||
|
fn call(
|
||||||
|
&mut self,
|
||||||
|
request: tonic::Request<super::RegisterRequest>,
|
||||||
|
) -> Self::Future {
|
||||||
|
let inner = Arc::clone(&self.0);
|
||||||
|
let fut = async move {
|
||||||
|
<T as AuthService>::register(&inner, request).await
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let accept_compression_encodings = self.accept_compression_encodings;
|
||||||
|
let send_compression_encodings = self.send_compression_encodings;
|
||||||
|
let max_decoding_message_size = self.max_decoding_message_size;
|
||||||
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
let fut = async move {
|
||||||
|
let method = RegisterSvc(inner);
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
|
.apply_compression_config(
|
||||||
|
accept_compression_encodings,
|
||||||
|
send_compression_encodings,
|
||||||
|
)
|
||||||
|
.apply_max_message_size_config(
|
||||||
|
max_decoding_message_size,
|
||||||
|
max_encoding_message_size,
|
||||||
|
);
|
||||||
|
let res = grpc.unary(method, req).await;
|
||||||
|
Ok(res)
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
"/multieko2.auth.AuthService/Login" => {
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
struct LoginSvc<T: AuthService>(pub Arc<T>);
|
||||||
|
impl<T: AuthService> tonic::server::UnaryService<super::LoginRequest>
|
||||||
|
for LoginSvc<T> {
|
||||||
|
type Response = super::LoginResponse;
|
||||||
|
type Future = BoxFuture<
|
||||||
|
tonic::Response<Self::Response>,
|
||||||
|
tonic::Status,
|
||||||
|
>;
|
||||||
|
fn call(
|
||||||
|
&mut self,
|
||||||
|
request: tonic::Request<super::LoginRequest>,
|
||||||
|
) -> Self::Future {
|
||||||
|
let inner = Arc::clone(&self.0);
|
||||||
|
let fut = async move {
|
||||||
|
<T as AuthService>::login(&inner, request).await
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let accept_compression_encodings = self.accept_compression_encodings;
|
||||||
|
let send_compression_encodings = self.send_compression_encodings;
|
||||||
|
let max_decoding_message_size = self.max_decoding_message_size;
|
||||||
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
let fut = async move {
|
||||||
|
let method = LoginSvc(inner);
|
||||||
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
|
.apply_compression_config(
|
||||||
|
accept_compression_encodings,
|
||||||
|
send_compression_encodings,
|
||||||
|
)
|
||||||
|
.apply_max_message_size_config(
|
||||||
|
max_decoding_message_size,
|
||||||
|
max_encoding_message_size,
|
||||||
|
);
|
||||||
|
let res = grpc.unary(method, req).await;
|
||||||
|
Ok(res)
|
||||||
|
};
|
||||||
|
Box::pin(fut)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut response = http::Response::new(empty_body());
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
headers
|
||||||
|
.insert(
|
||||||
|
tonic::Status::GRPC_STATUS,
|
||||||
|
(tonic::Code::Unimplemented as i32).into(),
|
||||||
|
);
|
||||||
|
headers
|
||||||
|
.insert(
|
||||||
|
http::header::CONTENT_TYPE,
|
||||||
|
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> Clone for AuthServiceServer<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
let inner = self.inner.clone();
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
accept_compression_encodings: self.accept_compression_encodings,
|
||||||
|
send_compression_encodings: self.send_compression_encodings,
|
||||||
|
max_decoding_message_size: self.max_decoding_message_size,
|
||||||
|
max_encoding_message_size: self.max_encoding_message_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Generated gRPC service name
|
||||||
|
pub const SERVICE_NAME: &str = "multieko2.auth.AuthService";
|
||||||
|
impl<T> tonic::server::NamedService for AuthServiceServer<T> {
|
||||||
|
const NAME: &'static str = SERVICE_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ dotenvy = "0.15.7"
|
|||||||
prost = "0.13.5"
|
prost = "0.13.5"
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
serde = { version = "1.0.218", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time"] }
|
sqlx = { version = "0.8.3", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "time", "uuid"] }
|
||||||
tokio = { version = "1.43.0", features = ["full", "macros"] }
|
tokio = { version = "1.43.0", features = ["full", "macros"] }
|
||||||
tonic = "0.12.3"
|
tonic = "0.12.3"
|
||||||
tonic-reflection = "0.12.3"
|
tonic-reflection = "0.12.3"
|
||||||
@@ -24,6 +24,10 @@ thiserror = "2.0.12"
|
|||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
bcrypt = "0.17.0"
|
||||||
|
validator = { version = "0.20.0", features = ["derive"] }
|
||||||
|
uuid = { version = "1.16.0", features = ["serde", "v4"] }
|
||||||
|
jsonwebtoken = "9.3.1"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "server"
|
name = "server"
|
||||||
|
|||||||
38
server/migrations/20250324192805_auth.sql
Normal file
38
server/migrations/20250324192805_auth.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(255) UNIQUE,
|
||||||
|
password_hash VARCHAR(255),
|
||||||
|
role VARCHAR(20) NOT NULL DEFAULT 'accountant',
|
||||||
|
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add an index for faster lookups
|
||||||
|
CREATE INDEX idx_users_email_username ON users(email, username);
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD CONSTRAINT valid_roles CHECK (role IN (
|
||||||
|
'admin',
|
||||||
|
'moderator',
|
||||||
|
'accountant',
|
||||||
|
'viewer'
|
||||||
|
));
|
||||||
|
|
||||||
|
-- Create JWT sessions table
|
||||||
|
CREATE TABLE user_sessions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
jwt_token TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
revoked BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add indexes
|
||||||
|
CREATE INDEX idx_sessions_user ON user_sessions(user_id);
|
||||||
|
CREATE INDEX idx_sessions_expires ON user_sessions(expires_at);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
10
server/src/auth/docs/first_reg.txt
Normal file
10
server/src/auth/docs/first_reg.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"username": "testuser2",
|
||||||
|
"email": "test2@example.com"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Register
|
||||||
|
{
|
||||||
|
"id": "5fa9bbce-85e0-4b06-8364-b561770c2fdd",
|
||||||
|
"username": "testuser2",
|
||||||
|
"email": "test2@example.com",
|
||||||
|
"role": "accountant"
|
||||||
|
}
|
||||||
51
server/src/auth/docs/reg_log.txt
Normal file
51
server/src/auth/docs/reg_log.txt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"username": "testuser3",
|
||||||
|
"email": "test3@example.com",
|
||||||
|
"password": "your_password",
|
||||||
|
"password_confirmation": "your_password"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Register
|
||||||
|
{
|
||||||
|
"id": "96d2fd35-b39d-4c05-916a-66134453d34c",
|
||||||
|
"username": "testuser3",
|
||||||
|
"email": "test3@example.com",
|
||||||
|
"role": "accountant"
|
||||||
|
}
|
||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"identifier": "testuser3"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Login
|
||||||
|
ERROR:
|
||||||
|
Code: Unauthenticated
|
||||||
|
Message: Invalid credentials
|
||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"identifier": "testuser3",
|
||||||
|
"password": "your_password"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Login
|
||||||
|
{
|
||||||
|
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5NmQyZmQzNS1iMzlkLTRjMDUtOTE2YS02NjEzNDQ1M2QzNGMiLCJleHAiOjE3NDI5ODE2MTAsInJvbGUiOiJhY2NvdW50YW50In0.78VIR3X4QZohzeI5x3xmkmqcICTusOC6PELPohMV-k8",
|
||||||
|
"tokenType": "Bearer",
|
||||||
|
"expiresIn": 86400,
|
||||||
|
"userId": "96d2fd35-b39d-4c05-916a-66134453d34c",
|
||||||
|
"role": "accountant"
|
||||||
|
}
|
||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"username": "testuser4",
|
||||||
|
"email": "test4@example.com"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Register
|
||||||
|
{
|
||||||
|
"id": "413d7ecc-f231-48af-8c5a-566b1dc2bf0b",
|
||||||
|
"username": "testuser4",
|
||||||
|
"email": "test4@example.com",
|
||||||
|
"role": "accountant"
|
||||||
|
}
|
||||||
|
❯ grpcurl -plaintext -d '{
|
||||||
|
"identifier": "test4@example.com"
|
||||||
|
}' localhost:50051 multieko2.auth.AuthService/Login
|
||||||
|
{
|
||||||
|
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MTNkN2VjYy1mMjMxLTQ4YWYtOGM1YS01NjZiMWRjMmJmMGIiLCJleHAiOjE3NDI5ODE3MDEsInJvbGUiOiJhY2NvdW50YW50In0.4Hzu3tTZRNGHnBSgeCbGy2tFTl8EzpPdXBhcW8kuIc8",
|
||||||
|
"tokenType": "Bearer",
|
||||||
|
"expiresIn": 86400,
|
||||||
|
"userId": "413d7ecc-f231-48af-8c5a-566b1dc2bf0b",
|
||||||
|
"role": "accountant"
|
||||||
|
}
|
||||||
|
╭─ ~/Doc/pr/multieko2/server auth ······ ✔
|
||||||
|
╰─
|
||||||
7
server/src/auth/handlers.rs
Normal file
7
server/src/auth/handlers.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// src/auth/handlers.rs
|
||||||
|
|
||||||
|
pub mod register;
|
||||||
|
pub mod login;
|
||||||
|
|
||||||
|
pub use register::*;
|
||||||
|
pub use login::*;
|
||||||
46
server/src/auth/handlers/login.rs
Normal file
46
server/src/auth/handlers/login.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// src/auth/handlers/login.rs
|
||||||
|
use bcrypt::verify;
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use crate::db::PgPool;
|
||||||
|
use crate::auth::{models::AuthError, logic::jwt}; // Fixed import path
|
||||||
|
use common::proto::multieko2::auth::{LoginRequest, LoginResponse};
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
pool: &PgPool,
|
||||||
|
request: LoginRequest,
|
||||||
|
) -> Result<Response<LoginResponse>, Status> {
|
||||||
|
let user = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT id, password_hash, role
|
||||||
|
FROM users
|
||||||
|
WHERE username = $1 OR email = $1
|
||||||
|
"#,
|
||||||
|
request.identifier
|
||||||
|
)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(e.to_string()))?
|
||||||
|
.ok_or_else(|| Status::unauthenticated("Invalid credentials"))?;
|
||||||
|
|
||||||
|
// Handle the optional password_hash
|
||||||
|
let password_hash = user.password_hash
|
||||||
|
.ok_or_else(|| Status::internal("User account has no password set"))?;
|
||||||
|
|
||||||
|
// Verify the password
|
||||||
|
if !verify(&request.password, &password_hash)
|
||||||
|
.map_err(|e| Status::internal(e.to_string()))?
|
||||||
|
{
|
||||||
|
return Err(Status::unauthenticated("Invalid credentials"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = jwt::generate_token(user.id, &user.role)
|
||||||
|
.map_err(|e| Status::internal(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Response::new(LoginResponse {
|
||||||
|
access_token: token,
|
||||||
|
token_type: "Bearer".to_string(),
|
||||||
|
expires_in: 86400, // 24 hours
|
||||||
|
user_id: user.id.to_string(),
|
||||||
|
role: user.role,
|
||||||
|
}))
|
||||||
|
}
|
||||||
48
server/src/auth/handlers/register.rs
Normal file
48
server/src/auth/handlers/register.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// src/auth/handlers/register.rs
|
||||||
|
use bcrypt::{hash, DEFAULT_COST};
|
||||||
|
use tonic::{Response, Status};
|
||||||
|
use common::proto::multieko2::auth::{RegisterRequest, AuthResponse};
|
||||||
|
use crate::db::PgPool;
|
||||||
|
use crate::auth::models::AuthError;
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
pool: &PgPool,
|
||||||
|
payload: RegisterRequest,
|
||||||
|
) -> Result<Response<AuthResponse>, Status> {
|
||||||
|
// Validate passwords match
|
||||||
|
if payload.password != payload.password_confirmation {
|
||||||
|
return Err(Status::invalid_argument(AuthError::PasswordMismatch.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
let password_hash = hash(payload.password, DEFAULT_COST)
|
||||||
|
.map_err(|e| Status::internal(AuthError::HashingError(e.to_string()).to_string()))?;
|
||||||
|
|
||||||
|
// Insert user
|
||||||
|
let user = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO users (username, email, password_hash, role)
|
||||||
|
VALUES ($1, $2, $3, 'accountant')
|
||||||
|
RETURNING id, username, email, role
|
||||||
|
"#,
|
||||||
|
payload.username,
|
||||||
|
payload.email,
|
||||||
|
password_hash
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.to_string().contains("duplicate key") {
|
||||||
|
Status::already_exists(AuthError::UserExists.to_string())
|
||||||
|
} else {
|
||||||
|
Status::internal(AuthError::DatabaseError(e.to_string()).to_string())
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Response::new(AuthResponse {
|
||||||
|
id: user.id.to_string(),
|
||||||
|
username: user.username,
|
||||||
|
email: user.email.unwrap_or_default(),
|
||||||
|
role: user.role,
|
||||||
|
}))
|
||||||
|
}
|
||||||
9
server/src/auth/logic.rs
Normal file
9
server/src/auth/logic.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// src/auth/logic.rs
|
||||||
|
|
||||||
|
pub mod jwt;
|
||||||
|
pub mod middleware;
|
||||||
|
// TODO implement RBAC on all of the endpoints
|
||||||
|
// pub mod rbac;
|
||||||
|
|
||||||
|
pub use jwt::*;
|
||||||
|
pub use middleware::*;
|
||||||
55
server/src/auth/logic/jwt.rs
Normal file
55
server/src/auth/logic/jwt.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// src/auth/jwt.rs
|
||||||
|
use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use crate::auth::models::AuthError;
|
||||||
|
|
||||||
|
static KEYS: OnceLock<Keys> = OnceLock::new();
|
||||||
|
|
||||||
|
struct Keys {
|
||||||
|
encoding: EncodingKey,
|
||||||
|
decoding: DecodingKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct Claims {
|
||||||
|
pub sub: Uuid, // User ID
|
||||||
|
pub exp: i64, // Expiration time
|
||||||
|
pub role: String, // User role
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_jwt() -> Result<(), AuthError> {
|
||||||
|
let secret = std::env::var("JWT_SECRET")
|
||||||
|
.map_err(|_| AuthError::ConfigError("JWT_SECRET must be set".to_string()))?;
|
||||||
|
|
||||||
|
KEYS.set(Keys {
|
||||||
|
encoding: EncodingKey::from_secret(secret.as_bytes()),
|
||||||
|
decoding: DecodingKey::from_secret(secret.as_bytes()),
|
||||||
|
}).map_err(|_| AuthError::ConfigError("Failed to initialize JWT keys".to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_token(user_id: Uuid, role: &str) -> Result<String, AuthError> {
|
||||||
|
let keys = KEYS.get().ok_or(AuthError::ConfigError("JWT not initialized".to_string()))?;
|
||||||
|
|
||||||
|
let exp = OffsetDateTime::now_utc() + Duration::days(365000);
|
||||||
|
let claims = Claims {
|
||||||
|
sub: user_id,
|
||||||
|
exp: exp.unix_timestamp(),
|
||||||
|
role: role.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
encode(&Header::default(), &claims, &keys.encoding)
|
||||||
|
.map_err(|e| AuthError::JwtError(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_token(token: &str) -> Result<Claims, AuthError> {
|
||||||
|
let keys = KEYS.get().ok_or(AuthError::ConfigError("JWT not initialized".to_string()))?;
|
||||||
|
|
||||||
|
decode::<Claims>(token, &keys.decoding, &Validation::default())
|
||||||
|
.map(|data| data.claims)
|
||||||
|
.map_err(|e| AuthError::JwtError(e.to_string()))
|
||||||
|
}
|
||||||
22
server/src/auth/logic/middleware.rs
Normal file
22
server/src/auth/logic/middleware.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// src/auth/middleware.rs
|
||||||
|
use tonic::{metadata::MetadataValue, service::Interceptor, Status};
|
||||||
|
use crate::auth::{logic::jwt, models::AuthError};
|
||||||
|
|
||||||
|
pub struct AuthInterceptor;
|
||||||
|
|
||||||
|
impl Interceptor for AuthInterceptor {
|
||||||
|
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, Status> {
|
||||||
|
let metadata = request.metadata();
|
||||||
|
let token = metadata.get("authorization")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.and_then(|s| s.strip_prefix("Bearer "))
|
||||||
|
.ok_or(Status::unauthenticated("Missing authorization header"))?;
|
||||||
|
|
||||||
|
let claims = jwt::validate_token(token)
|
||||||
|
.map_err(|e| Status::unauthenticated(e.to_string()))?;
|
||||||
|
|
||||||
|
// Store claims in request extensions
|
||||||
|
request.extensions_mut().insert(claims);
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
server/src/auth/logic/rbac.rs
Normal file
36
server/src/auth/logic/rbac.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// src/auth/logic/rbac.rs
|
||||||
|
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use crate::auth::logic::rbac;
|
||||||
|
|
||||||
|
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// ... existing setup code ...
|
||||||
|
|
||||||
|
// Create service layers
|
||||||
|
let adresar_layer = ServiceBuilder::new()
|
||||||
|
.layer(rbac::create_adresar_layer())
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
let uctovnictvo_layer = ServiceBuilder::new()
|
||||||
|
.layer(rbac::create_uctovnictvo_layer())
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
// Create services with layers
|
||||||
|
let adresar_service = AdresarServer::new(AdresarService { db_pool: db_pool.clone() })
|
||||||
|
.layer(adresar_layer);
|
||||||
|
|
||||||
|
let uctovnictvo_service = UctovnictvoServer::new(UctovnictvoService { db_pool: db_pool.clone() })
|
||||||
|
.layer(uctovnictvo_layer);
|
||||||
|
|
||||||
|
// ... repeat for other services ...
|
||||||
|
|
||||||
|
Server::builder()
|
||||||
|
.add_service(auth_server)
|
||||||
|
.add_service(adresar_service)
|
||||||
|
.add_service(uctovnictvo_service)
|
||||||
|
// ... other services ...
|
||||||
|
.serve(addr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
6
server/src/auth/mod.rs
Normal file
6
server/src/auth/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// src/auth/mod.rs
|
||||||
|
|
||||||
|
pub mod models;
|
||||||
|
pub mod logic;
|
||||||
|
pub mod handlers;
|
||||||
|
|
||||||
41
server/src/auth/models.rs
Normal file
41
server/src/auth/models.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// src/auth/models.rs
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Debug, Validate, Deserialize)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
#[validate(length(min = 1, max = 30))]
|
||||||
|
pub username: String,
|
||||||
|
#[validate(email)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(min = 1))]
|
||||||
|
pub password: String,
|
||||||
|
pub password_confirmation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Validate, Deserialize)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
#[validate(length(min = 1))]
|
||||||
|
pub identifier: String,
|
||||||
|
#[validate(length(min = 1))]
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("Passwords do not match")]
|
||||||
|
PasswordMismatch,
|
||||||
|
#[error("User already exists")]
|
||||||
|
UserExists,
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DatabaseError(String),
|
||||||
|
#[error("Hashing error: {0}")]
|
||||||
|
HashingError(String),
|
||||||
|
#[error("Invalid credentials")]
|
||||||
|
InvalidCredentials,
|
||||||
|
#[error("JWT error: {0}")]
|
||||||
|
JwtError(String),
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
ConfigError(String),
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
// src/db.rs
|
// src/db.rs
|
||||||
use sqlx::postgres::{PgPool, PgPoolOptions};
|
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
pub use sqlx::postgres::PgPool;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/lib.rs
|
// src/lib.rs
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod auth;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod adresar;
|
pub mod adresar;
|
||||||
pub mod uctovnictvo;
|
pub mod uctovnictvo;
|
||||||
|
|||||||
@@ -10,15 +10,22 @@ use crate::server::services::{
|
|||||||
TableDefinitionService,
|
TableDefinitionService,
|
||||||
TablesDataService,
|
TablesDataService,
|
||||||
TableScriptService,
|
TableScriptService,
|
||||||
|
AuthServiceImpl
|
||||||
|
};
|
||||||
|
use common::proto::multieko2::{
|
||||||
|
adresar::adresar_server::AdresarServer,
|
||||||
|
uctovnictvo::uctovnictvo_server::UctovnictvoServer,
|
||||||
|
table_structure::table_structure_service_server::TableStructureServiceServer,
|
||||||
|
table_definition::table_definition_server::TableDefinitionServer,
|
||||||
|
tables_data::tables_data_server::TablesDataServer,
|
||||||
|
table_script::table_script_server::TableScriptServer,
|
||||||
|
auth::auth_service_server::AuthServiceServer
|
||||||
};
|
};
|
||||||
use common::proto::multieko2::adresar::adresar_server::AdresarServer;
|
|
||||||
use common::proto::multieko2::uctovnictvo::uctovnictvo_server::UctovnictvoServer;
|
|
||||||
use common::proto::multieko2::table_structure::table_structure_service_server::TableStructureServiceServer;
|
|
||||||
use common::proto::multieko2::table_definition::table_definition_server::TableDefinitionServer;
|
|
||||||
use common::proto::multieko2::tables_data::tables_data_server::TablesDataServer;
|
|
||||||
use common::proto::multieko2::table_script::table_script_server::TableScriptServer;
|
|
||||||
|
|
||||||
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Initialize JWT for authentication
|
||||||
|
crate::auth::logic::jwt::init_jwt()?;
|
||||||
|
|
||||||
let addr = "[::1]:50051".parse()?;
|
let addr = "[::1]:50051".parse()?;
|
||||||
|
|
||||||
let reflection_service = ReflectionBuilder::configure()
|
let reflection_service = ReflectionBuilder::configure()
|
||||||
@@ -27,8 +34,9 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
|
|||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
|
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
|
||||||
let tables_data_service = TablesDataService { db_pool: db_pool.clone() }; // Add this
|
let tables_data_service = TablesDataService { db_pool: db_pool.clone() };
|
||||||
let table_script_service = TableScriptService { db_pool: db_pool.clone() };
|
let table_script_service = TableScriptService { db_pool: db_pool.clone() };
|
||||||
|
let auth_service = AuthServiceImpl { db_pool: db_pool.clone() };
|
||||||
|
|
||||||
Server::builder()
|
Server::builder()
|
||||||
.add_service(AdresarServer::new(AdresarService { db_pool: db_pool.clone() }))
|
.add_service(AdresarServer::new(AdresarService { db_pool: db_pool.clone() }))
|
||||||
@@ -37,6 +45,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
|
|||||||
.add_service(TableDefinitionServer::new(table_definition_service))
|
.add_service(TableDefinitionServer::new(table_definition_service))
|
||||||
.add_service(TablesDataServer::new(tables_data_service))
|
.add_service(TablesDataServer::new(tables_data_service))
|
||||||
.add_service(TableScriptServer::new(table_script_service))
|
.add_service(TableScriptServer::new(table_script_service))
|
||||||
|
.add_service(AuthServiceServer::new(auth_service))
|
||||||
.add_service(reflection_service)
|
.add_service(reflection_service)
|
||||||
.serve(addr)
|
.serve(addr)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
36
server/src/server/services/auth_service.rs
Normal file
36
server/src/server/services/auth_service.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// src/server/services/auth_service.rs
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use common::proto::multieko2::auth::{
|
||||||
|
auth_service_server::AuthService,
|
||||||
|
RegisterRequest, AuthResponse,
|
||||||
|
LoginRequest, LoginResponse
|
||||||
|
};
|
||||||
|
use crate::auth::handlers::{
|
||||||
|
login::login,
|
||||||
|
register::register
|
||||||
|
};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AuthServiceImpl {
|
||||||
|
pub db_pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl AuthService for AuthServiceImpl {
|
||||||
|
async fn register(
|
||||||
|
&self,
|
||||||
|
request: Request<RegisterRequest>,
|
||||||
|
) -> Result<Response<AuthResponse>, Status> {
|
||||||
|
let response = register(&self.db_pool, request.into_inner()).await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(
|
||||||
|
&self,
|
||||||
|
request: Request<LoginRequest>,
|
||||||
|
) -> Result<Response<LoginResponse>, Status> {
|
||||||
|
let response = login(&self.db_pool, request.into_inner()).await?;
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ pub mod uctovnictvo_service;
|
|||||||
pub mod table_definition_service;
|
pub mod table_definition_service;
|
||||||
pub mod tables_data_service;
|
pub mod tables_data_service;
|
||||||
pub mod table_script_service;
|
pub mod table_script_service;
|
||||||
|
pub mod auth_service;
|
||||||
|
|
||||||
pub use adresar_service::AdresarService;
|
pub use adresar_service::AdresarService;
|
||||||
pub use table_structure_service::TableStructureHandler;
|
pub use table_structure_service::TableStructureHandler;
|
||||||
@@ -13,3 +14,4 @@ pub use uctovnictvo_service::UctovnictvoService;
|
|||||||
pub use table_definition_service::TableDefinitionService;
|
pub use table_definition_service::TableDefinitionService;
|
||||||
pub use tables_data_service::TablesDataService;
|
pub use tables_data_service::TablesDataService;
|
||||||
pub use table_script_service::TableScriptService;
|
pub use table_script_service::TableScriptService;
|
||||||
|
pub use auth_service::AuthServiceImpl;
|
||||||
|
|||||||
Reference in New Issue
Block a user